Files
2026-05-08 15:23:29 +03:00

749 lines
24 KiB
HTML
Raw Permalink 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.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Админ-панель - Управление вопросами викторины</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.admin-container {
max-width: 1400px;
margin: 0 auto;
}
/* Header */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 15px;
margin-bottom: 30px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 15px;
}
.header h1 {
font-size: 28px;
}
.header-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: all 0.3s ease;
}
.btn-primary {
background: #4CAF50;
color: white;
}
.btn-primary:hover {
background: #45a049;
transform: translateY(-2px);
}
.btn-danger {
background: #f44336;
color: white;
}
.btn-danger:hover {
background: #da190b;
transform: translateY(-2px);
}
.btn-warning {
background: #ff9800;
color: white;
}
.btn-warning:hover {
background: #e68900;
transform: translateY(-2px);
}
.btn-info {
background: #2196F3;
color: white;
}
.btn-info:hover {
background: #0b7dda;
transform: translateY(-2px);
}
.btn-secondary {
background: #9e9e9e;
color: white;
}
.btn-secondary:hover {
background: #757575;
transform: translateY(-2px);
}
/* Stats */
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 15px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
}
.stat-number {
font-size: 36px;
font-weight: bold;
color: #667eea;
}
.stat-label {
color: #666;
margin-top: 5px;
}
/* Search and filter */
.search-bar {
background: white;
padding: 20px;
border-radius: 15px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.search-input {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
}
.search-input:focus {
outline: none;
border-color: #667eea;
}
/* Questions list */
.questions-list {
background: white;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.question-item {
border-bottom: 1px solid #e0e0e0;
padding: 20px;
transition: background 0.3s;
}
.question-item:hover {
background: #f9f9f9;
}
.question-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 10px;
flex-wrap: wrap;
gap: 10px;
}
.question-number {
font-weight: bold;
color: #667eea;
font-size: 18px;
}
.question-actions {
display: flex;
gap: 10px;
}
.question-text {
font-size: 16px;
font-weight: 500;
margin-bottom: 10px;
color: #333;
}
.question-options {
margin: 10px 0;
padding-left: 20px;
}
.option {
font-size: 14px;
color: #666;
margin: 5px 0;
}
.option.correct {
color: #4CAF50;
font-weight: bold;
}
.question-explanation {
font-size: 13px;
color: #999;
font-style: italic;
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed #e0e0e0;
}
/* Modal */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 20px;
max-width: 600px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-header h2 {
color: #333;
}
.close-modal {
font-size: 28px;
cursor: pointer;
color: #999;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #333;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 10px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
font-family: inherit;
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
.options-group {
margin-bottom: 15px;
padding: 10px;
background: #f9f9f9;
border-radius: 8px;
}
.option-input {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 10px;
}
.option-input input {
flex: 1;
}
.form-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 20px;
}
/* Alert */
.alert {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 10px;
color: white;
font-weight: bold;
z-index: 1100;
animation: slideIn 0.3s ease;
}
.alert-success {
background: #4CAF50;
}
.alert-error {
background: #f44336;
}
.alert-info {
background: #2196F3;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Empty state */
.empty-state {
text-align: center;
padding: 60px;
color: #999;
}
/* Responsive */
@media (max-width: 768px) {
.header {
flex-direction: column;
text-align: center;
}
.question-header {
flex-direction: column;
}
.question-actions {
width: 100%;
justify-content: flex-end;
}
}
</style>
</head>
<body>
<div class="admin-container">
<div class="header">
<h1>📝 Админ-панель управления вопросами</h1>
<div class="header-buttons">
<button class="btn btn-primary" onclick="openAddModal()"> Добавить вопрос</button>
<button class="btn btn-info" onclick="exportQuestions()">📤 Экспорт</button>
<button class="btn btn-warning" onclick="importQuestions()">📥 Импорт</button>
<button class="btn btn-danger" onclick="resetQuestions()">🔄 Сбросить</button>
<a href="/" class="btn btn-secondary" target="_blank">🎮 Перейти в викторину</a>
</div>
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-number" id="totalQuestions">0</div>
<div class="stat-label">Всего вопросов</div>
</div>
<div class="stat-card">
<div class="stat-number" id="activeGames">0</div>
<div class="stat-label">Активных игр</div>
</div>
</div>
<div class="search-bar">
<input type="text" class="search-input" id="searchInput" placeholder="🔍 Поиск вопросов...">
</div>
<div class="questions-list" id="questionsList">
<div class="empty-state">Загрузка вопросов...</div>
</div>
</div>
<!-- Модальное окно добавления/редактирования -->
<div id="modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modalTitle">Добавить вопрос</h2>
<span class="close-modal" onclick="closeModal()">&times;</span>
</div>
<form id="questionForm">
<div class="form-group">
<label>Вопрос *</label>
<textarea id="questionText" required placeholder="Введите текст вопроса..."></textarea>
</div>
<div class="form-group">
<label>Варианты ответов *</label>
<div id="optionsContainer">
<div class="options-group">
<div class="option-input">
<input type="text" class="option-input-0" placeholder="Вариант 1" required>
</div>
<div class="option-input">
<input type="text" class="option-input-1" placeholder="Вариант 2" required>
</div>
<div class="option-input">
<input type="text" class="option-input-2" placeholder="Вариант 3" required>
</div>
<div class="option-input">
<input type="text" class="option-input-3" placeholder="Вариант 4" required>
</div>
</div>
</div>
</div>
<div class="form-group">
<label>Правильный ответ *</label>
<select id="correctAnswer" required>
<option value="0">Вариант 1</option>
<option value="1">Вариант 2</option>
<option value="2">Вариант 3</option>
<option value="3">Вариант 4</option>
</select>
</div>
<div class="form-group">
<label>Пояснение</label>
<textarea id="explanation" placeholder="Пояснение к правильному ответу..."></textarea>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="closeModal()">Отмена</button>
<button type="submit" class="btn btn-primary">Сохранить</button>
</div>
</form>
</div>
</div>
<script>
let questions = [];
let editingIndex = null;
// Загрузка вопросов
async function loadQuestions() {
try {
const response = await fetch('/api/questions');
questions = await response.json();
document.getElementById('totalQuestions').textContent = questions.length;
renderQuestions();
} catch (error) {
console.error('Ошибка:', error);
showAlert('Ошибка загрузки вопросов', 'error');
}
}
// Отображение вопросов
function renderQuestions() {
const container = document.getElementById('questionsList');
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const filtered = questions.filter(q =>
q.text.toLowerCase().includes(searchTerm) ||
q.options.some(opt => opt.toLowerCase().includes(searchTerm))
);
if (filtered.length === 0) {
container.innerHTML = '<div class="empty-state">📭 Вопросы не найдены</div>';
return;
}
container.innerHTML = filtered.map((q, idx) => {
const originalIndex = questions.findIndex(orig => orig === q);
return `
<div class="question-item">
<div class="question-header">
<span class="question-number">Вопрос #${originalIndex + 1}</span>
<div class="question-actions">
<button class="btn btn-info" onclick="editQuestion(${originalIndex})" style="padding: 5px 15px;">✏️ Редактировать</button>
<button class="btn btn-danger" onclick="deleteQuestion(${originalIndex})" style="padding: 5px 15px;">🗑️ Удалить</button>
</div>
</div>
<div class="question-text">${escapeHtml(q.text)}</div>
<div class="question-options">
${q.options.map((opt, optIdx) => `
<div class="option ${optIdx === q.correct ? 'correct' : ''}">
${String.fromCharCode(65+optIdx)}. ${escapeHtml(opt)} ${optIdx === q.correct ? '✓' : ''}
</div>
`).join('')}
</div>
${q.explanation ? `<div class="question-explanation">💡 ${escapeHtml(q.explanation)}</div>` : ''}
</div>
`;
}).join('');
}
// Экранирование HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Открыть модальное окно для добавления
function openAddModal() {
editingIndex = null;
document.getElementById('modalTitle').textContent = '➕ Добавить вопрос';
document.getElementById('questionForm').reset();
document.getElementById('correctAnswer').value = '0';
document.getElementById('modal').classList.add('active');
}
// Редактирование вопроса
function editQuestion(index) {
editingIndex = index;
const q = questions[index];
document.getElementById('modalTitle').textContent = '✏️ Редактировать вопрос';
document.getElementById('questionText').value = q.text;
document.getElementById('correctAnswer').value = q.correct;
document.getElementById('explanation').value = q.explanation || '';
// Заполняем варианты ответов
q.options.forEach((opt, i) => {
const input = document.querySelector(`.option-input-${i}`);
if (input) input.value = opt;
});
document.getElementById('modal').classList.add('active');
}
// Сохранение вопроса
document.getElementById('questionForm').addEventListener('submit', async (e) => {
e.preventDefault();
const options = [];
for (let i = 0; i < 4; i++) {
const input = document.querySelector(`.option-input-${i}`);
if (!input.value.trim()) {
showAlert('Заполните все варианты ответов', 'error');
return;
}
options.push(input.value.trim());
}
const questionData = {
text: document.getElementById('questionText').value.trim(),
options: options,
correct: parseInt(document.getElementById('correctAnswer').value),
explanation: document.getElementById('explanation').value.trim()
};
if (!questionData.text) {
showAlert('Введите текст вопроса', 'error');
return;
}
try {
let response;
if (editingIndex !== null) {
response = await fetch(`/api/questions/${editingIndex}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(questionData)
});
} else {
response = await fetch('/api/questions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(questionData)
});
}
const result = await response.json();
if (result.success) {
showAlert(result.message, 'success');
closeModal();
loadQuestions();
} else {
showAlert(result.message, 'error');
}
} catch (error) {
showAlert('Ошибка сохранения', 'error');
}
});
// Удаление вопроса
async function deleteQuestion(index) {
if (confirm('Вы уверены, что хотите удалить этот вопрос?')) {
try {
const response = await fetch(`/api/questions/${index}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showAlert(result.message, 'success');
loadQuestions();
} else {
showAlert(result.message, 'error');
}
} catch (error) {
showAlert('Ошибка удаления', 'error');
}
}
}
// Сброс вопросов
async function resetQuestions() {
if (confirm('Сбросить все вопросы к стандартному пулу? Все добавленные вопросы будут удалены!')) {
try {
const response = await fetch('/api/questions/reset', {
method: 'POST'
});
const result = await response.json();
if (result.success) {
showAlert(result.message, 'success');
loadQuestions();
}
} catch (error) {
showAlert('Ошибка сброса', 'error');
}
}
}
// Экспорт вопросов
async function exportQuestions() {
try {
const response = await fetch('/api/questions/export');
const data = await response.json();
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `questions_export_${new Date().toISOString().slice(0,19)}.json`;
a.click();
URL.revokeObjectURL(url);
showAlert('Вопросы экспортированы', 'success');
} catch (error) {
showAlert('Ошибка экспорта', 'error');
}
}
// Импорт вопросов
async function importQuestions() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = async (e) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = async (event) => {
try {
const questions = JSON.parse(event.target.result);
const response = await fetch('/api/questions/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(questions)
});
const result = await response.json();
if (result.success) {
showAlert(result.message, 'success');
loadQuestions();
} else {
showAlert(result.message, 'error');
}
} catch (error) {
showAlert('Ошибка парсинга файла', 'error');
}
};
reader.readAsText(file);
};
input.click();
}
// Показать уведомление
function showAlert(message, type) {
const alert = document.createElement('div');
alert.className = `alert alert-${type}`;
alert.textContent = message;
document.body.appendChild(alert);
setTimeout(() => alert.remove(), 3000);
}
// Закрыть модальное окно
function closeModal() {
document.getElementById('modal').classList.remove('active');
editingIndex = null;
}
// Поиск
document.getElementById('searchInput').addEventListener('input', () => {
renderQuestions();
});
// Закрытие модального окна по клику вне его
document.getElementById('modal').addEventListener('click', (e) => {
if (e.target === document.getElementById('modal')) {
closeModal();
}
});
// Загрузка активных игр
async function loadActiveGames() {
// Можно добавить эндпоинт для подсчёта активных игр
document.getElementById('activeGames').textContent = Object.keys(activeGames || {}).length;
}
// Инициализация
loadQuestions();
setInterval(loadActiveGames, 5000);
</script>
</body>
</html>