fix путей

This commit is contained in:
Primakov Alexandr Alexandrovich
2025-03-12 00:09:36 +03:00
parent 1fcc5ed70d
commit dd589790c2
12 changed files with 1997 additions and 529 deletions

View File

@@ -3,13 +3,27 @@ $(document).ready(function() {
const adminLink = window.location.pathname.split('/').pop();
let questionnaireData = null;
// Получаем базовый путь API (для работы и с /questioneer, и с /ms/questioneer)
// Функция для получения базового пути API
const getApiPath = () => {
const pathParts = window.location.pathname.split('/');
// Убираем последние две части пути (admin/:adminLink)
pathParts.pop();
pathParts.pop();
return pathParts.join('/') + '/api';
// Проверяем, содержит ли путь /ms/ (продакшн на dev.bro-js.ru)
const pathname = window.location.pathname;
const isMsPath = pathname.includes('/ms/questioneer');
if (isMsPath) {
// Для продакшна: если в пути есть /ms/, то API доступно по /ms/questioneer/api
return '/ms/questioneer/api';
} else {
// Для локальной разработки: используем обычный путь
// Извлекаем базовый путь из URL страницы
const pathParts = pathname.split('/');
// Если последний сегмент пустой (из-за /) - удаляем его
if (pathParts[pathParts.length - 1] === '') {
pathParts.pop();
}
// Путь до корня приложения
return pathParts.join('/') + '/api';
}
};
// Загрузка данных опроса
@@ -40,7 +54,17 @@ $(document).ready(function() {
// Формируем ссылки
const baseUrl = window.location.origin;
const baseQuestionnairePath = window.location.pathname.split('/admin')[0];
const isMsPath = window.location.pathname.includes('/ms/questioneer');
let baseQuestionnairePath;
if (isMsPath) {
// Для продакшна: используем /ms/questioneer
baseQuestionnairePath = '/ms/questioneer';
} else {
// Для локальной разработки: используем текущий путь
baseQuestionnairePath = window.location.pathname.split('/admin')[0];
}
const publicUrl = `${baseUrl}${baseQuestionnairePath}/poll/${questionnaireData.publicLink}`;
const adminUrl = `${baseUrl}${baseQuestionnairePath}/admin/${questionnaireData.adminLink}`;
@@ -65,23 +89,32 @@ $(document).ready(function() {
// Проверяем наличие ответов для каждого типа вопросов
for (const question of questions) {
if (question.type === 'single' || question.type === 'multiple') {
if (question.options && question.options.some(option => option.votes && option.votes > 0)) {
// Согласовываем типы вопросов между бэкендом и фронтендом
const questionType = normalizeQuestionType(question.type);
if (questionType === 'single' || questionType === 'multiple') {
if (question.options && question.options.some(option => (option.votes > 0 || option.count > 0))) {
hasAnyResponses = true;
break;
}
} else if (question.type === 'tagcloud') {
if (question.tags && question.tags.some(tag => tag.count && tag.count > 0)) {
} else if (questionType === 'tagcloud') {
if (question.tags && question.tags.some(tag => tag.count > 0)) {
hasAnyResponses = true;
break;
}
} else if (question.type === 'scale' || question.type === 'rating') {
if (question.responses && question.responses.length > 0) {
} else if (questionType === 'scale' || questionType === 'rating') {
// Проверяем оба возможных поля для данных шкалы
const hasScaleValues = question.scaleValues && question.scaleValues.length > 0;
const hasResponses = question.responses && question.responses.length > 0;
if (hasScaleValues || hasResponses) {
hasAnyResponses = true;
break;
}
} else if (question.type === 'text') {
if (question.textAnswers && question.textAnswers.length > 0) {
} else if (questionType === 'text') {
// Проверяем оба возможных поля для текстовых ответов
const hasTextAnswers = question.textAnswers && question.textAnswers.length > 0;
const hasAnswers = question.answers && question.answers.length > 0;
if (hasTextAnswers || hasAnswers) {
hasAnyResponses = true;
break;
}
@@ -99,138 +132,256 @@ $(document).ready(function() {
const $questionTitle = $('<h3>', { text: `${index + 1}. ${question.text}` });
$questionStats.append($questionTitle);
// Согласовываем типы вопросов между бэкендом и фронтендом
const questionType = normalizeQuestionType(question.type);
// В зависимости от типа вопроса отображаем разную статистику
if (question.type === 'single' || question.type === 'multiple') {
if (questionType === 'single' || questionType === 'multiple') {
// Для вопросов с выбором вариантов
const totalVotes = question.options.reduce((sum, option) => sum + (option.votes || 0), 0);
if (totalVotes === 0) {
$questionStats.append($('<div>', { class: 'no-votes', text: 'Нет голосов' }));
} else {
const $table = $('<table>', { class: 'stats-table' });
const $thead = $('<thead>').append(
$('<tr>').append(
$('<th>', { text: 'Вариант' }),
$('<th>', { text: 'Голоса' }),
$('<th>', { text: '%' }),
$('<th>', { text: 'Визуализация' })
)
);
const $tbody = $('<tbody>');
question.options.forEach(option => {
const votes = option.votes || 0;
const percent = totalVotes > 0 ? Math.round((votes / totalVotes) * 100) : 0;
const $tr = $('<tr>').append(
$('<td>', { text: option.text }),
$('<td>', { text: votes }),
$('<td>', { text: `${percent}%` }),
$('<td>').append(
$('<div>', { class: 'bar-container' }).append(
$('<div>', {
class: 'bar',
css: { width: `${percent}%` }
})
)
)
);
$tbody.append($tr);
});
$table.append($thead, $tbody);
$questionStats.append($table);
$questionStats.append($('<div>', { class: 'total-votes', text: `Всего голосов: ${totalVotes}` }));
}
} else if (question.type === 'tagcloud') {
renderChoiceStats(question, $questionStats);
} else if (questionType === 'tagcloud') {
// Для облака тегов
if (!question.tags || question.tags.length === 0 || !question.tags.some(tag => tag.count > 0)) {
$questionStats.append($('<div>', { class: 'no-votes', text: 'Нет выбранных тегов' }));
} else {
const $tagCloud = $('<div>', { class: 'tag-cloud-stats' });
// Находим максимальное количество для масштабирования
const maxCount = Math.max(...question.tags.map(tag => tag.count || 0));
// Сортируем теги по популярности
const sortedTags = [...question.tags].sort((a, b) => (b.count || 0) - (a.count || 0));
sortedTags.forEach(tag => {
if (tag.count && tag.count > 0) {
const fontSize = maxCount > 0 ? 1 + (tag.count / maxCount) * 1.5 : 1; // от 1em до 2.5em
$tagCloud.append(
$('<span>', {
class: 'tag-item',
text: `${tag.text} (${tag.count})`,
css: { fontSize: `${fontSize}em` }
})
);
}
});
$questionStats.append($tagCloud);
}
} else if (question.type === 'scale' || question.type === 'rating') {
renderTagCloudStats(question, $questionStats);
} else if (questionType === 'scale' || questionType === 'rating') {
// Для шкалы и рейтинга
if (!question.responses || question.responses.length === 0) {
$questionStats.append($('<div>', { class: 'no-votes', text: 'Нет оценок' }));
} else {
const values = question.responses;
const sum = values.reduce((a, b) => a + b, 0);
const avg = sum / values.length;
const min = Math.min(...values);
const max = Math.max(...values);
const $scaleStats = $('<div>', { class: 'scale-stats' });
$scaleStats.append(
$('<div>', { class: 'stat-item' }).append(
$('<span>', { class: 'stat-label', text: 'Среднее значение:' }),
$('<span>', { class: 'stat-value', text: avg.toFixed(1) })
),
$('<div>', { class: 'stat-item' }).append(
$('<span>', { class: 'stat-label', text: 'Минимум:' }),
$('<span>', { class: 'stat-value', text: min })
),
$('<div>', { class: 'stat-item' }).append(
$('<span>', { class: 'stat-label', text: 'Максимум:' }),
$('<span>', { class: 'stat-value', text: max })
),
$('<div>', { class: 'stat-item' }).append(
$('<span>', { class: 'stat-label', text: 'Количество оценок:' }),
$('<span>', { class: 'stat-value', text: values.length })
)
);
$questionStats.append($scaleStats);
}
} else if (question.type === 'text') {
renderScaleStats(question, $questionStats);
} else if (questionType === 'text') {
// Для текстовых ответов
if (!question.textAnswers || question.textAnswers.length === 0) {
$questionStats.append($('<div>', { class: 'no-votes', text: 'Нет текстовых ответов' }));
} else {
const $textAnswers = $('<div>', { class: 'text-answers-list' });
question.textAnswers.forEach((answer, i) => {
$textAnswers.append(
$('<div>', { class: 'text-answer-item' }).append(
$('<div>', { class: 'answer-number', text: `#${i + 1}` }),
$('<div>', { class: 'answer-text', text: answer })
)
);
});
$questionStats.append($textAnswers);
}
renderTextStats(question, $questionStats);
}
$statsContainer.append($questionStats);
});
};
// Приводит тип вопроса к стандартному формату
const normalizeQuestionType = (type) => {
const typeMap = {
'single_choice': 'single',
'multiple_choice': 'multiple',
'tag_cloud': 'tagcloud',
'single': 'single',
'multiple': 'multiple',
'tagcloud': 'tagcloud',
'scale': 'scale',
'rating': 'rating',
'text': 'text'
};
return typeMap[type] || type;
};
// Отображение статистики для вопросов с выбором
const renderChoiceStats = (question, $container) => {
// Преобразуем опции к единому формату
const options = question.options.map(option => ({
text: option.text,
votes: option.votes || option.count || 0
}));
const totalVotes = options.reduce((sum, option) => sum + option.votes, 0);
if (totalVotes === 0) {
$container.append($('<div>', { class: 'no-votes', text: 'Нет голосов' }));
return;
}
const $table = $('<table>', { class: 'stats-table' });
const $thead = $('<thead>').append(
$('<tr>').append(
$('<th>', { text: 'Вариант' }),
$('<th>', { text: 'Голоса' }),
$('<th>', { text: '%' }),
$('<th>', { text: 'Визуализация' })
)
);
const $tbody = $('<tbody>');
options.forEach(option => {
const votes = option.votes;
const percent = totalVotes > 0 ? Math.round((votes / totalVotes) * 100) : 0;
const $tr = $('<tr>').append(
$('<td>', { text: option.text }),
$('<td>', { text: votes }),
$('<td>', { text: `${percent}%` }),
$('<td>').append(
$('<div>', { class: 'bar-container' }).append(
$('<div>', {
class: 'bar',
css: { width: `${percent}%` }
})
)
)
);
$tbody.append($tr);
});
$table.append($thead, $tbody);
$container.append($table);
$container.append($('<div>', { class: 'total-votes', text: `Всего голосов: ${totalVotes}` }));
};
// Отображение статистики для облака тегов
const renderTagCloudStats = (question, $container) => {
if (!question.tags || question.tags.length === 0 || !question.tags.some(tag => tag.count > 0)) {
$container.append($('<div>', { class: 'no-votes', text: 'Нет выбранных тегов' }));
return;
}
const $tagCloud = $('<div>', { class: 'tag-cloud-stats' });
// Находим максимальное количество для масштабирования
const maxCount = Math.max(...question.tags.map(tag => tag.count || 0));
// Сортируем теги по популярности
const sortedTags = [...question.tags].sort((a, b) => (b.count || 0) - (a.count || 0));
sortedTags.forEach(tag => {
if (tag.count && tag.count > 0) {
const fontSize = maxCount > 0 ? 1 + (tag.count / maxCount) * 1.5 : 1; // от 1em до 2.5em
$tagCloud.append(
$('<span>', {
class: 'tag-item',
text: `${tag.text} (${tag.count})`,
css: { fontSize: `${fontSize}em` }
})
);
}
});
$container.append($tagCloud);
};
// Отображение статистики для шкалы и рейтинга
const renderScaleStats = (question, $container) => {
// Используем scaleValues или responses, в зависимости от того, что доступно
const values = question.responses && question.responses.length > 0
? question.responses
: (question.scaleValues || []);
if (values.length === 0) {
$container.append($('<div>', { class: 'no-votes', text: 'Нет оценок' }));
return;
}
const sum = values.reduce((a, b) => a + b, 0);
const avg = sum / values.length;
const min = Math.min(...values);
const max = Math.max(...values);
// Создаем контейнер для статистики
const $scaleStats = $('<div>', { class: 'scale-stats' });
// Добавляем сводную статистику
$scaleStats.append(
$('<div>', { class: 'stat-summary' }).append(
$('<div>', { class: 'stat-item' }).append(
$('<span>', { class: 'stat-label', text: 'Среднее значение:' }),
$('<span>', { class: 'stat-value', text: avg.toFixed(1) })
),
$('<div>', { class: 'stat-item' }).append(
$('<span>', { class: 'stat-label', text: 'Минимум:' }),
$('<span>', { class: 'stat-value', text: min })
),
$('<div>', { class: 'stat-item' }).append(
$('<span>', { class: 'stat-label', text: 'Максимум:' }),
$('<span>', { class: 'stat-value', text: max })
),
$('<div>', { class: 'stat-item' }).append(
$('<span>', { class: 'stat-label', text: 'Количество оценок:' }),
$('<span>', { class: 'stat-value', text: values.length })
)
)
);
// Создаем таблицу для визуализации распределения голосов
const $table = $('<table>', { class: 'stats-table' });
const $thead = $('<thead>').append(
$('<tr>').append(
$('<th>', { text: 'Значение' }),
$('<th>', { text: 'Голоса' }),
$('<th>', { text: '%' }),
$('<th>', { text: 'Визуализация' })
)
);
const $tbody = $('<tbody>');
// Определяем минимальное и максимальное значение шкалы из самих данных
// либо используем значения из настроек вопроса, если они есть
const scaleMin = question.scaleMin !== undefined ? question.scaleMin : min;
const scaleMax = question.scaleMax !== undefined ? question.scaleMax : max;
// Создаем счетчик для каждого возможного значения шкалы
const countByValue = {};
for (let i = scaleMin; i <= scaleMax; i++) {
countByValue[i] = 0;
}
// Подсчитываем количество голосов для каждого значения
values.forEach(value => {
if (countByValue[value] !== undefined) {
countByValue[value]++;
}
});
// Создаем строки таблицы для каждого значения шкалы
for (let value = scaleMin; value <= scaleMax; value++) {
const count = countByValue[value] || 0;
const percent = values.length > 0 ? Math.round((count / values.length) * 100) : 0;
const $tr = $('<tr>').append(
$('<td>', { text: value }),
$('<td>', { text: count }),
$('<td>', { text: `${percent}%` }),
$('<td>').append(
$('<div>', { class: 'bar-container' }).append(
$('<div>', {
class: 'bar',
css: { width: `${percent}%` }
})
)
)
);
$tbody.append($tr);
}
$table.append($thead, $tbody);
$scaleStats.append($table);
$container.append($scaleStats);
};
// Отображение статистики для текстовых ответов
const renderTextStats = (question, $container) => {
// Используем textAnswers или answers, в зависимости от того, что доступно
const answers = question.textAnswers && question.textAnswers.length > 0
? question.textAnswers
: (question.answers || []);
if (answers.length === 0) {
$container.append($('<div>', { class: 'no-votes', text: 'Нет текстовых ответов' }));
return;
}
const $textAnswers = $('<div>', { class: 'text-answers-list' });
answers.forEach((answer, i) => {
$textAnswers.append(
$('<div>', { class: 'text-answer-item' }).append(
$('<div>', { class: 'answer-number', text: `#${i + 1}` }),
$('<div>', { class: 'answer-text', text: answer })
)
);
});
$container.append($textAnswers);
};
// Копирование ссылок
$('#copy-public-link').on('click', function() {
$('#public-link').select();
@@ -252,7 +403,18 @@ $(document).ready(function() {
// Редактирование опроса
$('#edit-questionnaire').on('click', function() {
const basePath = window.location.pathname.split('/admin')[0];
// Перенаправляем на страницу редактирования
const isMsPath = window.location.pathname.includes('/ms/questioneer');
let basePath;
if (isMsPath) {
// Для продакшна: используем /ms/questioneer
basePath = '/ms/questioneer';
} else {
// Для локальной разработки: используем текущий путь
basePath = window.location.pathname.split('/admin')[0];
}
window.location.href = `${basePath}/edit/${adminLink}`;
});
@@ -268,7 +430,7 @@ $(document).ready(function() {
// Функция удаления опроса
const deleteQuestionnaire = () => {
$.ajax({
url: `${getApiPath()}/questionnaires/admin/${adminLink}`,
url: `${getApiPath()}/questionnaires/${adminLink}`,
method: 'DELETE',
success: function(result) {
if (result.success) {

View File

@@ -6,12 +6,27 @@ $(document).ready(function() {
let questionCount = 0;
// Получаем базовый путь API (для работы и с /questioneer, и с /ms/questioneer)
// Функция для получения базового пути API
const getApiPath = () => {
const pathParts = window.location.pathname.split('/');
// Убираем последнюю часть пути (create)
pathParts.pop();
return pathParts.join('/') + '/api';
// Проверяем, содержит ли путь /ms/ (продакшн на dev.bro-js.ru)
const pathname = window.location.pathname;
const isMsPath = pathname.includes('/ms/questioneer');
if (isMsPath) {
// Для продакшна: если в пути есть /ms/, то API доступно по /ms/questioneer/api
return '/ms/questioneer/api';
} else {
// Для локальной разработки: используем обычный путь
// Извлекаем базовый путь из URL страницы
const pathParts = pathname.split('/');
// Если последний сегмент пустой (из-за /) - удаляем его
if (pathParts[pathParts.length - 1] === '') {
pathParts.pop();
}
// Путь до корня приложения
return pathParts.join('/') + '/api';
}
};
// Добавление нового вопроса
@@ -217,8 +232,18 @@ $(document).ready(function() {
data: JSON.stringify(questionnaire),
success: function(result) {
if (result.success) {
// Перенаправление на страницу администрирования опроса
const basePath = window.location.pathname.split('/create')[0];
// Перенаправляем на страницу администратора
const isMsPath = window.location.pathname.includes('/ms/questioneer');
let basePath;
if (isMsPath) {
// Для продакшна: используем /ms/questioneer
basePath = '/ms/questioneer';
} else {
// Для локальной разработки: используем текущий путь
basePath = window.location.pathname.split('/create')[0];
}
window.location.href = `${basePath}/admin/${result.data.adminLink}`;
} else {
showAlert(`Ошибка при создании опроса: ${result.error}`, 'Ошибка');

View File

@@ -8,13 +8,27 @@ $(document).ready(function() {
let questionCount = 0;
let questionnaireData = null;
// Получаем базовый путь API
// Функция для получения базового пути API
const getApiPath = () => {
const pathParts = window.location.pathname.split('/');
// Убираем последние две части пути (edit/:adminLink)
pathParts.pop();
pathParts.pop();
return pathParts.join('/') + '/api';
// Проверяем, содержит ли путь /ms/ (продакшн на dev.bro-js.ru)
const pathname = window.location.pathname;
const isMsPath = pathname.includes('/ms/questioneer');
if (isMsPath) {
// Для продакшна: если в пути есть /ms/, то API доступно по /ms/questioneer/api
return '/ms/questioneer/api';
} else {
// Для локальной разработки: используем обычный путь
// Извлекаем базовый путь из URL страницы
const pathParts = pathname.split('/');
// Если последний сегмент пустой (из-за /) - удаляем его
if (pathParts[pathParts.length - 1] === '') {
pathParts.pop();
}
// Путь до корня приложения
return pathParts.join('/') + '/api';
}
};
// Загрузка данных опроса
@@ -312,10 +326,20 @@ $(document).ready(function() {
data: JSON.stringify(questionnaire),
success: function(result) {
if (result.success) {
showAlert('Опрос успешно обновлен', 'Успешно', function() {
const basePath = window.location.pathname.split('/edit')[0];
window.location.href = `${basePath}/admin/${adminLink}`;
});
showAlert('Опрос успешно сохранен!', 'Успех', { autoClose: true });
// Перенаправляем на страницу администратора
const isMsPath = window.location.pathname.includes('/ms/questioneer');
let basePath;
if (isMsPath) {
// Для продакшна: используем /ms/questioneer
basePath = '/ms/questioneer';
} else {
// Для локальной разработки: используем текущий путь
basePath = window.location.pathname.split('/edit')[0];
}
window.location.href = `${basePath}/admin/${adminLink}`;
} else {
showAlert(`Ошибка при обновлении опроса: ${result.error}`, 'Ошибка');
}

View File

@@ -2,15 +2,25 @@
$(document).ready(function() {
// Функция для получения базового пути API
const getApiPath = () => {
// Извлекаем базовый путь из URL страницы
const pathParts = window.location.pathname.split('/');
// Если последний сегмент пустой (из-за /) - удаляем его
if (pathParts[pathParts.length - 1] === '') {
pathParts.pop();
}
// Проверяем, содержит ли путь /ms/ (продакшн на dev.bro-js.ru)
const pathname = window.location.pathname;
const isMsPath = pathname.includes('/ms/questioneer');
// Путь до корня приложения
return pathParts.join('/') + '/api';
if (isMsPath) {
// Для продакшна: если в пути есть /ms/, то API доступно по /ms/questioneer/api
return '/ms/questioneer/api';
} else {
// Для локальной разработки: используем обычный путь
// Извлекаем базовый путь из URL страницы
const pathParts = pathname.split('/');
// Если последний сегмент пустой (из-за /) - удаляем его
if (pathParts[pathParts.length - 1] === '') {
pathParts.pop();
}
// Путь до корня приложения
return pathParts.join('/') + '/api';
}
};
// Функция для загрузки списка опросов
@@ -40,9 +50,18 @@ $(document).ready(function() {
}
// Получаем базовый путь (для работы и с /questioneer, и с /ms/questioneer)
const basePath = window.location.pathname.endsWith('/')
? window.location.pathname
: window.location.pathname + '/';
const basePath = (() => {
const pathname = window.location.pathname;
const isMsPath = pathname.includes('/ms/questioneer');
if (isMsPath) {
// Для продакшна: нужно использовать /ms/questioneer/ для ссылок
return '/ms/questioneer/';
} else {
// Для локальной разработки: используем текущий путь
return pathname.endsWith('/') ? pathname : pathname + '/';
}
})();
const questionnairesHTML = questionnaires.map(q => `
<div class="questionnaire-item">

View File

@@ -23,6 +23,9 @@ $(document).ready(function() {
// Для пошаговых опросов
let currentQuestionIndex = 0;
// Объявляем переменную для хранения накопленных ответов
let accumulatedAnswers = [];
// Проверка доступности localStorage
const isLocalStorageAvailable = () => {
try {
@@ -50,13 +53,33 @@ $(document).ready(function() {
window.localStorage.setItem(getLocalStorageKey(), 'true');
};
// Получаем базовый путь API
// Функция для получения базового пути API
const getApiPath = () => {
const pathParts = window.location.pathname.split('/');
// Убираем последние две части пути (poll/:publicLink)
pathParts.pop();
pathParts.pop();
return pathParts.join('/') + '/api';
// Проверяем, содержит ли путь /ms/ (продакшн на dev.bro-js.ru)
const pathname = window.location.pathname;
const isMsPath = pathname.includes('/ms/questioneer');
if (isMsPath) {
// Для продакшна: если в пути есть /ms/, то API доступно по /ms/questioneer/api
return '/ms/questioneer/api';
} else {
// Для локальной разработки: используем обычный путь
// Извлекаем базовый путь из URL страницы
const pathParts = pathname.split('/');
// Убираем slug и последний сегмент (poll/[id])
if (pathParts.length > 2) {
// Удаляем 2 последних сегмента пути (poll/[id])
pathParts.pop();
pathParts.pop();
}
// Если последний сегмент пустой (из-за /) - удаляем его
if (pathParts[pathParts.length - 1] === '') {
pathParts.pop();
}
// Путь до корня приложения
return pathParts.join('/') + '/api';
}
};
// Загрузка данных опроса
@@ -231,7 +254,7 @@ $(document).ready(function() {
</svg>
Назад
</button>
<span id="question-counter">Вопрос 1 из ${questionnaireData.questions.length}</span>
<span id="question-counter" class="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">
@@ -317,6 +340,9 @@ $(document).ready(function() {
return;
}
// Сохраняем ответ на текущий вопрос
saveCurrentAnswer(currentQuestion, currentQuestionIndex);
// Если есть еще вопросы, переходим к следующему
if (currentQuestionIndex < questionnaireData.questions.length - 1) {
// Анимируем текущий вопрос перед переходом к следующему
@@ -343,6 +369,114 @@ $(document).ready(function() {
}
};
// Функция для сохранения ответа на текущий вопрос
const saveCurrentAnswer = (question, questionIndex) => {
// Нормализуем тип вопроса для согласованной обработки
const questionType = normalizeQuestionType(question.type);
const $questionEl = $(`.question-item[data-index="${questionIndex}"]`);
// Удаляем предыдущий ответ на этот вопрос, если он был
accumulatedAnswers = accumulatedAnswers.filter(answer => answer.questionIndex !== questionIndex);
let optionIndices;
let selectedOption;
let selectedOptions;
let selectedTags;
let textAnswer;
let scaleValue;
switch (questionType) {
case 'single':
selectedOption = $questionEl.find(`input[name="question_${questionIndex}"]:checked`);
if (selectedOption.length) {
accumulatedAnswers.push({
questionIndex,
optionIndices: [parseInt(selectedOption.val())]
});
}
break;
case 'multiple':
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]);
});
accumulatedAnswers.push({
questionIndex,
optionIndices
});
}
break;
case 'text':
textAnswer = $questionEl.find('textarea').val().trim();
if (textAnswer) {
accumulatedAnswers.push({
questionIndex,
textAnswer
});
}
break;
case 'scale':
case 'rating': // Обратная совместимость
selectedOption = $questionEl.find(`input[name="question_${questionIndex}"]:checked`);
if (selectedOption.length) {
const scaleValue = parseInt(selectedOption.val());
accumulatedAnswers.push({
questionIndex,
scaleValue,
optionIndices: [scaleValue]
});
}
break;
case 'tagcloud':
selectedTags = $questionEl.find('.tag-item.selected');
if (selectedTags.length) {
const tagTexts = $.map(selectedTags, function(tag) {
return $(tag).text().replace('×', '').trim();
});
accumulatedAnswers.push({
questionIndex,
tagTexts
});
}
break;
}
};
// Отправка формы
const submitForm = function() {
// Отключаем атрибут required у невидимых полей перед отправкой
$('input[required]:not(:visible), textarea[required]:not(:visible)').prop('required', false);
// Отправляем накопленные ответы на сервер
$.ajax({
url: `${getApiPath()}/vote/${publicLink}`,
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ answers: accumulatedAnswers }),
success: function(result) {
if (result.success) {
// Сохраняем информацию о прохождении опроса
markAsCompleted();
// Показываем анимацию благодарности вместо сразу скрытия формы
showThankYouAnimation();
} else {
showAlert(`Ошибка: ${result.error}`, 'Ошибка');
}
},
error: function(error) {
console.error('Error submitting poll:', error);
showAlert('Не удалось отправить ответы. Пожалуйста, попробуйте позже.', 'Ошибка');
}
});
};
// Проверка наличия ответа на вопрос
const checkQuestionAnswer = (question, questionIndex) => {
switch (question.type) {
@@ -605,7 +739,7 @@ $(document).ready(function() {
const $tagInput = $('<input>', {
type: 'text',
placeholder: 'Введите теги, разделенные пробелом, и нажмите Enter',
placeholder: 'Введите тег и нажмите Enter',
class: 'tag-input'
});
@@ -620,32 +754,25 @@ $(document).ready(function() {
const inputText = $(this).val().trim();
if (inputText) {
// Разбиваем ввод на отдельные теги
const tags = inputText.split(/\s+/);
// Добавляем тег как отдельный элемент
const $tagItem = $('<div>', {
class: 'tag-item selected',
text: inputText
});
// Добавляем каждый тег как отдельный элемент
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);
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();
}
@@ -679,11 +806,74 @@ $(document).ready(function() {
}
};
// Создание анимации конфети
const createConfetti = () => {
// Создаем контейнер для конфети, если его еще нет
let $confettiContainer = $('.confetti-container');
if ($confettiContainer.length === 0) {
$confettiContainer = $('<div>', { class: 'confetti-container' });
$('body').append($confettiContainer);
} else {
$confettiContainer.empty(); // Очищаем предыдущие конфети
}
// Параметры конфети
const confettiCount = 100; // Количество конфети
const shapes = ['square', 'circle', 'triangle']; // Формы конфети
const colors = ['red', 'blue', 'green', 'yellow', 'purple']; // Цвета конфети
// Создаем конфети
for (let i = 0; i < confettiCount; i++) {
// Случайные параметры для конфети
const shape = shapes[Math.floor(Math.random() * shapes.length)];
const color = colors[Math.floor(Math.random() * colors.length)];
const left = Math.random() * 100; // Позиция по горизонтали (%)
const size = Math.random() * 10 + 5; // Размер (px)
// Случайные параметры для анимации
const fallDuration = Math.random() * 3 + 2; // Длительность падения (s)
const swayDuration = Math.random() * 2 + 1; // Длительность качания (s)
const fallDelay = Math.random() * 2; // Задержка начала падения (s)
const swayDelay = Math.random() * 1; // Задержка начала качания (s)
// Создаем элемент конфети
const $confetti = $('<div>', {
class: `confetti ${shape} ${color}`,
css: {
left: `${left}%`,
width: `${size}px`,
height: `${size}px`,
animation: `confetti-fall ${fallDuration}s linear ${fallDelay}s forwards, confetti-sway ${swayDuration}s ease-in-out ${swayDelay}s infinite`
}
});
// Если это треугольник, корректируем размер
if (shape === 'triangle') {
$confetti.css({
'border-width': `0 ${size/2}px ${size*0.87}px ${size/2}px`
});
}
// Добавляем конфети в контейнер
$confettiContainer.append($confetti);
}
// Удаляем контейнер после завершения анимации
setTimeout(() => {
$confettiContainer.fadeOut(1000, function() {
$(this).remove();
});
}, 5000);
};
// Показываем анимацию благодарности
const showThankYouAnimation = () => {
// Скрываем форму
formEl.hide();
// Создаем анимацию конфети
createConfetti();
// Создаем анимацию благодарности
const $thankYouEl = $('<div>', {
class: 'thank-you-animation',
@@ -730,109 +920,6 @@ $(document).ready(function() {
}, 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({
@@ -864,6 +951,15 @@ $(document).ready(function() {
pollResultsContainerEl.append($resultsTitle);
// Проверяем наличие вопросов в данных
if (!data.questions || data.questions.length === 0) {
pollResultsContainerEl.append($('<p>', {
text: 'Нет данных для отображения',
class: 'no-results'
}));
return;
}
// Отображаем результаты с отложенной анимацией
data.questions.forEach((question, index) => {
setTimeout(() => {
@@ -879,159 +975,24 @@ $(document).ready(function() {
$questionResult.append($questionTitle);
// Нормализация типа вопроса
const questionType = normalizeQuestionType(question.type);
// Отображаем результаты в зависимости от типа вопроса
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'
}));
}
}
switch (questionType) {
case 'single':
case 'multiple':
renderChoiceResults(question, $questionResult);
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'
}));
}
case 'tagcloud':
renderTagCloudResults(question, $questionResult);
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'
}));
}
renderScaleResults(question, $questionResult);
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'
}));
}
renderTextResults(question, $questionResult);
break;
}
@@ -1043,14 +1004,196 @@ $(document).ready(function() {
opacity: 1,
transform: 'translateY(0)'
});
}, 100);
}, index * 300); // Задержка между вопросами
}, index * 300); // Задержка между вопросами
});
});
};
// Нормализация типа вопроса
const normalizeQuestionType = (type) => {
const typeMap = {
'single_choice': 'single',
'multiple_choice': 'multiple',
'tag_cloud': 'tagcloud',
'single': 'single',
'multiple': 'multiple',
'tagcloud': 'tagcloud',
'scale': 'scale',
'rating': 'rating',
'text': 'text'
};
return typeMap[type] || type;
};
// Отображение результатов для вопросов с выбором
const renderChoiceResults = (question, $container) => {
// Адаптируем формат опций
const options = question.options || [];
// Если нет данных для отображения
if (!data.questions || data.questions.length === 0) {
pollResultsContainerEl.append($('<p>', {
text: 'Нет данных для отображения',
// Находим общее количество голосов
const totalVotes = options.reduce((sum, option) => sum + (option.count || option.votes || 0), 0);
if (totalVotes > 0) {
options.forEach((option) => {
const votes = option.count || option.votes || 0;
const percent = totalVotes > 0 ? Math.round(votes * 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}% (${votes} голосов)`
});
$resultBar.append($resultBarFill);
$optionResult.append($optionLabel, $resultBar, $resultPercent);
$container.append($optionResult);
// Анимируем заполнение полосы после добавления в DOM
setTimeout(() => {
$resultBarFill.css('width', `${percent}%`);
}, 100);
});
} else {
$container.append($('<p>', {
text: 'Пока нет голосов для этого вопроса',
class: 'no-results'
}));
}
};
// Отображение результатов для облака тегов
const renderTagCloudResults = (question, $container) => {
// Используем правильное поле для тегов
const tags = question.tags || [];
if (tags.length > 0 && tags.some(tag => (tag.count || 0) > 0)) {
const $tagCloud = $('<div>', {
class: 'results-tag-cloud'
});
// Находим максимальное количество для масштабирования
const maxCount = Math.max(...tags.map(tag => tag.count || 1));
tags.forEach((tag, tagIndex) => {
if (!tag.text) return; // Пропускаем некорректные теги
// Масштабируем размер тега от 1 до 3 в зависимости от частоты
const scale = maxCount > 1 ? 1 + ((tag.count || 1) / 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);
});
$container.append($tagCloud);
} else {
$container.append($('<p>', {
text: 'Пока нет тегов для этого вопроса',
class: 'no-results'
}));
}
};
// Отображение результатов для шкалы и рейтинга
const renderScaleResults = (question, $container) => {
// Используем доступное поле для значений шкалы
const values = question.responses && question.responses.length > 0
? question.responses
: (question.scaleValues || []);
if (values.length > 0) {
// Используем уже рассчитанное среднее или рассчитываем сами
const average = question.scaleAverage !== undefined
? question.scaleAverage
: (values.reduce((sum, val) => sum + val, 0) / values.length);
const min = Math.min(...values);
const max = Math.max(...values);
const $scaleAverage = $('<div>', {
class: 'scale-average',
html: `Средняя оценка: <span class="highlight">${average.toFixed(1)}</span> (мин: ${min}, макс: ${max})`
});
$container.append($scaleAverage);
} else {
$container.append($('<p>', {
text: 'Пока нет оценок для этого вопроса',
class: 'no-results'
}));
}
};
// Отображение результатов для текстовых вопросов
const renderTextResults = (question, $container) => {
// Используем доступное поле для текстовых ответов
const answers = question.textAnswers && question.textAnswers.length > 0
? question.textAnswers
: (question.answers || []);
if (answers.length > 0) {
const $answersContainer = $('<div>', {
class: 'text-answers-container'
});
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);
});
$container.append($answersContainer);
} else {
$container.append($('<p>', {
text: 'Пока нет текстовых ответов для этого вопроса',
class: 'no-results'
}));
}