diff --git a/.gitignore b/.gitignore index 0854a35..5c52eef 100644 --- a/.gitignore +++ b/.gitignore @@ -124,4 +124,8 @@ temp/ # Session files flask_session/ -sessions/ \ No newline at end of file +sessions/ + +# Добавить в существующий .gitignore +questions_custom.json +*.export.json \ No newline at end of file diff --git a/.idea/quiz_dnr.iml b/.idea/quiz_dnr.iml index d0876a7..f823063 100644 --- a/.idea/quiz_dnr.iml +++ b/.idea/quiz_dnr.iml @@ -1,8 +1,10 @@ - - + + + + \ No newline at end of file diff --git a/server.py b/server.py index 9171b23..0320a2c 100644 --- a/server.py +++ b/server.py @@ -2,24 +2,248 @@ from flask import Flask, render_template, request, jsonify, session, send_file from questions import QUESTION_POOL import random import uuid -import qrcode -from io import BytesIO import os import json from datetime import datetime +from io import BytesIO +from PIL import Image, ImageDraw, ImageFont, ImageOps, ImageFilter + +# Пытаемся импортировать qrcode с обработкой ошибок +try: + import qrcode + from qrcode.constants import ERROR_CORRECT_L + + QRCODE_AVAILABLE = True +except ImportError: + QRCODE_AVAILABLE = False + print("Warning: qrcode library not installed. Install with: pip install qrcode[pil]") app = Flask(__name__) -app.secret_key = 'dnr_youth_parliament_secret_key_2024' +app.secret_key = 'dnr_youth_parliament_secretary_key_2024' # Хранилище сессий игроков и результатов active_games = {} -saved_results = {} # Сохраняем результаты для QR-кодов +saved_results = {} + +# Файл для сохранения вопросов +QUESTIONS_FILE = 'questions_custom.json' + + +def load_questions(): + """Загружает вопросы из файла или использует стандартный пул""" + if os.path.exists(QUESTIONS_FILE): + with open(QUESTIONS_FILE, 'r', encoding='utf-8') as f: + return json.load(f) + return QUESTION_POOL.copy() + + +def save_questions(questions): + """Сохраняет вопросы в файл""" + with open(QUESTIONS_FILE, 'w', encoding='utf-8') as f: + json.dump(questions, f, ensure_ascii=False, indent=2) + + +def create_result_image(result_data): + """Создаёт изображение с результатами викторины в формате 16:9""" + # Формат 16:9 (1920x1080) + width = 1920 + height = 1080 + + # Создаём изображение с градиентным фоном + img = Image.new('RGB', (width, height)) + draw = ImageDraw.Draw(img) + + # Новый градиентный фон (тёмный градиент) + for i in range(height): + r = int(18 + (i / height) * 35) # RGB(18, 25, 45) -> RGB(53, 66, 89) + g = int(25 + (i / height) * 41) + b = int(45 + (i / height) * 44) + draw.line([(0, i), (width, i)], fill=(r, g, b)) + + # Декоративные элементы + # Большой круг в правом верхнем углу + circle1_radius = 300 + draw.ellipse([width - circle1_radius - 50, -circle1_radius // 2, + width + 50, circle1_radius + 50], + fill=(255, 100, 100, 30), outline=None) + + # Круг в левом нижнем углу + circle2_radius = 250 + draw.ellipse([-circle2_radius // 2, height - circle2_radius - 50, + circle2_radius + 50, height + 50], + fill=(100, 150, 255, 30), outline=None) + + # Золотая рамка с двойным контуром + border_margin = 15 + border_color = (218, 165, 32) # Золотой + + # Внешняя рамка + draw.rectangle([border_margin, border_margin, + width - border_margin, height - border_margin], + outline=border_color, width=3) + + # Внутренняя рамка + draw.rectangle([border_margin + 10, border_margin + 10, + width - border_margin - 10, height - border_margin - 10], + outline=border_color, width=1) + + # Угловые украшения + corner_length = 60 + corner_width = 5 + + # Верхний левый угол + draw.line([border_margin, border_margin + corner_length, + border_margin, border_margin], fill=border_color, width=corner_width) + draw.line([border_margin + corner_length, border_margin, + border_margin, border_margin], fill=border_color, width=corner_width) + + # Верхний правый угол + draw.line([width - border_margin, border_margin + corner_length, + width - border_margin, border_margin], fill=border_color, width=corner_width) + draw.line([width - border_margin - corner_length, border_margin, + width - border_margin, border_margin], fill=border_color, width=corner_width) + + # Нижний левый угол + draw.line([border_margin, height - border_margin - corner_length, + border_margin, height - border_margin], fill=border_color, width=corner_width) + draw.line([border_margin + corner_length, height - border_margin, + border_margin, height - border_margin], fill=border_color, width=corner_width) + + # Нижний правый угол + draw.line([width - border_margin, height - border_margin - corner_length, + width - border_margin, height - border_margin], fill=border_color, width=corner_width) + draw.line([width - border_margin - corner_length, height - border_margin, + width - border_margin, height - border_margin], fill=border_color, width=corner_width) + + # Пытаемся загрузить шрифты + try: + font_title = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 64) + font_score = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 160) + font_percent = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 56) + font_normal = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 32) + font_small = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 24) + except: + try: + # Альтернативные шрифты для разных ОС + font_title = ImageFont.truetype("arial.ttf", 64) + font_score = ImageFont.truetype("arial.ttf", 160) + font_percent = ImageFont.truetype("arial.ttf", 56) + font_normal = ImageFont.truetype("arial.ttf", 32) + font_small = ImageFont.truetype("arial.ttf", 24) + except: + font_title = ImageFont.load_default() + font_score = ImageFont.load_default() + font_percent = ImageFont.load_default() + font_normal = ImageFont.load_default() + font_small = ImageFont.load_default() + + # Центральный элемент - звезда/символ + center_x = width // 2 + + # Заголовок с тенью + title1 = "МОЛОДЁЖНЫЙ ПАРЛАМЕНТ" + title1_shadow = "МОЛОДЁЖНЫЙ ПАРЛАМЕНТ" + shadow_offset = 3 + + # Тень для заголовка + title1_width = draw.textlength(title1, font=font_title) + draw.text(((width - title1_width) // 2 + shadow_offset, 130 + shadow_offset), + title1_shadow, fill=(30, 30, 50), font=font_title) + draw.text(((width - title1_width) // 2, 130), title1, fill='white', font=font_title) + + # Подзаголовок + title2 = "ДОНЕЦКОЙ НАРОДНОЙ РЕСПУБЛИКИ" + title2_width = draw.textlength(title2, font=font_normal) + draw.text(((width - title2_width) // 2 + shadow_offset, 210 + shadow_offset), + title2, fill=(30, 30, 50), font=font_normal) + draw.text(((width - title2_width) // 2, 210), title2, fill=(218, 165, 32), font=font_normal) + + # Декоративная линия с точками + line_y = 270 + line_length = 400 + for i in range(0, line_length, 15): + x1 = center_x - line_length // 2 + i + if i % 30 == 0: + draw.ellipse([x1 - 3, line_y - 3, x1 + 3, line_y + 3], fill=(218, 165, 32)) + else: + draw.line([x1, line_y, x1 + 10, line_y], fill=(218, 165, 32), width=2) + + # Карточка с результатами + card_width = 600 + card_height = 380 # Увеличена высота карточки + card_x = (width - card_width) // 2 + card_y = 310 # Немного поднята карточка + + # Полупрозрачная карточка + card_overlay = Image.new('RGBA', (card_width, card_height), (0, 0, 0, 0)) + card_draw = ImageDraw.Draw(card_overlay) + card_draw.rectangle([0, 0, card_width, card_height], fill=(255, 255, 255, 30)) + + # Рамка карточки + card_draw.rectangle([0, 0, card_width, card_height], outline=(218, 165, 32), width=2) + + # Вставляем карточку + img.paste(card_overlay, (card_x, card_y), card_overlay) + + # Основной счёт (поднят выше) + score_y = card_y + 70 + score_text = f"{result_data['score']} / {result_data['total']}" + score_width = draw.textlength(score_text, font=font_score) + draw.text(((width - score_width) // 2, score_y), score_text, fill=(255, 215, 0), font=font_score) + + # Проценты (опущены ниже) + percent_y = card_y + 230 # Было 200, стало 230 - опустили на 30px + percent_text = f"{result_data['percentage']}%" + percent_width = draw.textlength(percent_text, font=font_percent) + + # Радужный градиент для процентов + draw.text(((width - percent_width) // 2, percent_y), percent_text, fill=(255, 215, 0), font=font_percent) + + # Звание (опущено ниже) + grade_y = card_y + 310 # Было 280, стало 310 - опустили на 30px + grade_text = result_data['grade'] + grade_width = draw.textlength(grade_text, font=font_normal) + draw.text(((width - grade_width) // 2, grade_y), grade_text, fill=(255, 215, 0), font=font_normal) + + # Нижняя часть + footer_y = height - 60 + + # Левая часть - QR-код + if QRCODE_AVAILABLE: + try: + result_url = f"http://localhost:5000/api/result/{result_data['game_id']}" + qr = qrcode.QRCode(version=1, box_size=4, border=2) + qr.add_data(result_url) + qr.make(fit=True) + qr_img = qr.make_image(fill_color=(218, 165, 32), back_color=(18, 25, 45)) + qr_img = qr_img.resize((100, 100)) + img.paste(qr_img, (80, footer_y - 70)) + except: + pass + + # Центральная часть - девиз + footer_text = "МОЛОДОСТЬ. ПАТРИОТИЗМ. РАЗВИТИЕ." + footer_width = draw.textlength(footer_text, font=font_small) + draw.text(((width - footer_width) // 2, footer_y), footer_text, fill=(218, 165, 32), font=font_small) + + # Правая часть - дата + date_text = datetime.now().strftime("%d.%m.%Y") + date_width = draw.textlength(date_text, font=font_small) + draw.text((width - date_width - 80, footer_y), date_text, fill=(150, 150, 180), font=font_small) + + # Подпись + sign_y = footer_y + 35 + sign_text = "Молодёжный Парламент Донецкой Народной Республики" + sign_width = draw.textlength(sign_text, font=font_small) + draw.text(((width - sign_width) // 2, sign_y), sign_text, fill=(100, 100, 130), font=font_small) + + return img class QuizGame: def __init__(self): self.game_id = str(uuid.uuid4())[:8] - self.questions_pool = QUESTION_POOL.copy() + self.questions_pool = load_questions() self.selected_questions = [] self.current_question_index = 0 self.score = 0 @@ -35,18 +259,18 @@ class QuizGame: else: self.selected_questions = self.questions_pool.copy() - # Добавляем тайминг к каждому вопросу for q in self.selected_questions: q['time_limit'] = 10 def get_current_question(self): - """Возвращает текущий вопрос (без правильного ответа)""" + """Возвращает текущий вопрос (с правильным ответом для подсветки)""" if self.current_question_index < len(self.selected_questions): q = self.selected_questions[self.current_question_index] return { 'id': self.current_question_index, 'text': q['text'], 'options': q['options'], + 'correct': q['correct'], 'time_limit': q.get('time_limit', 10) } return None @@ -56,7 +280,6 @@ class QuizGame: current_q = self.selected_questions[self.current_question_index] is_correct = (answer_index == current_q['correct']) - # Сохраняем ответ пользователя self.user_answers.append({ 'question_text': current_q['text'], 'options': current_q['options'], @@ -87,21 +310,19 @@ class QuizGame: total = len(self.selected_questions) percentage = (self.score / total) * 100 - # Определение звания/результата if percentage >= 90: - grade = "🏆 Депутат высшей категории!" + grade = "ДЕПУТАТ ВЫСШЕЙ КАТЕГОРИИ" message = "Вы отлично знаете историю и структуру Молодёжного Парламента ДНР!" elif percentage >= 70: - grade = "⭐ Активный парламентарий!" + grade = "АКТИВНЫЙ ПАРЛАМЕНТАРИЙ" message = "Хороший результат! Вы достойно показали свои знания." elif percentage >= 50: - grade = "📚 Кандидат в парламент!" + grade = "КАНДИДАТ В ПАРЛАМЕНТ" message = "Неплохо, но есть куда расти. Рекомендуем изучить материалы о работе Парламента." else: - grade = "🌱 Будущий лидер!" + grade = "БУДУЩИЙ ЛИДЕР" message = "Не расстраивайтесь! Участие в викторине - первый шаг к большим достижениям." - # Сохраняем результат для QR-кода result_data = { 'game_id': self.game_id, 'score': self.score, @@ -121,7 +342,7 @@ class QuizGame: 'grade': grade, 'message': message, 'questions_summary': self.user_answers, - 'result_id': self.game_id # Возвращаем ID для QR-кода + 'result_id': self.game_id } @@ -130,6 +351,87 @@ def index(): return render_template('index.html') +@app.route('/admin') +def admin(): + return render_template('admin.html') + + +@app.route('/api/questions', methods=['GET']) +def get_questions(): + questions = load_questions() + return jsonify(questions) + + +@app.route('/api/questions', methods=['POST']) +def add_question(): + data = request.json + questions = load_questions() + + new_question = { + 'text': data['text'], + 'options': data['options'], + 'correct': data['correct'], + 'explanation': data.get('explanation', '') + } + + questions.append(new_question) + save_questions(questions) + + return jsonify({'success': True, 'message': 'Вопрос добавлен', 'question': new_question}) + + +@app.route('/api/questions/', methods=['PUT']) +def update_question(index): + data = request.json + questions = load_questions() + + if 0 <= index < len(questions): + questions[index] = { + 'text': data['text'], + 'options': data['options'], + 'correct': data['correct'], + 'explanation': data.get('explanation', '') + } + save_questions(questions) + return jsonify({'success': True, 'message': 'Вопрос обновлён'}) + + return jsonify({'success': False, 'message': 'Вопрос не найден'}), 404 + + +@app.route('/api/questions/', methods=['DELETE']) +def delete_question(index): + questions = load_questions() + + if 0 <= index < len(questions): + deleted = questions.pop(index) + save_questions(questions) + return jsonify({'success': True, 'message': 'Вопрос удалён', 'question': deleted}) + + return jsonify({'success': False, 'message': 'Вопрос не найден'}), 404 + + +@app.route('/api/questions/reset', methods=['POST']) +def reset_questions(): + from questions import QUESTION_POOL + save_questions(QUESTION_POOL.copy()) + return jsonify({'success': True, 'message': 'Вопросы сброшены к стандартному пулу'}) + + +@app.route('/api/questions/export', methods=['GET']) +def export_questions(): + questions = load_questions() + return jsonify(questions) + + +@app.route('/api/questions/import', methods=['POST']) +def import_questions(): + data = request.json + if isinstance(data, list) and len(data) > 0: + save_questions(data) + return jsonify({'success': True, 'message': f'Импортировано {len(data)} вопросов'}) + return jsonify({'success': False, 'message': 'Неверный формат данных'}), 400 + + @app.route('/api/new_game', methods=['POST']) def new_game(): game = QuizGame() @@ -155,14 +457,12 @@ def check_answer(): result = game.check_answer(answer) if result['is_finished']: - # Игра завершена final_result = game.get_result() return jsonify({ 'is_finished': True, 'result': final_result }) else: - # Следующий вопрос return jsonify({ 'is_finished': False, 'result': result, @@ -173,31 +473,14 @@ def check_answer(): @app.route('/api/result/') -def get_result_page(result_id): - """Страница с результатами по QR-коду""" +def get_result_image(result_id): + """Возвращает изображение с результатами в формате 16:9""" result = saved_results.get(result_id) if not result: return "Результат не найден", 404 - return render_template('result_page.html', result=result) - - -@app.route('/api/qrcode/') -def generate_qrcode(result_id): - """Генерация QR-кода со ссылкой на результат""" - result_url = request.host_url.rstrip('/') + f'/api/result/{result_id}' - - # Создаём QR-код - qr = qrcode.QRCode( - version=1, - error_correction=qrcode.constants.ERROR_CORRECT_L, - box_size=10, - border=4, - ) - qr.add_data(result_url) - qr.make(fit=True) # Создаём изображение - img = qr.make_image(fill_color="black", back_color="white") + img = create_result_image(result) # Сохраняем в байты img_io = BytesIO() @@ -207,5 +490,50 @@ def generate_qrcode(result_id): return send_file(img_io, mimetype='image/png') +@app.route('/api/qrcode/') +def generate_qrcode(result_id): + if not QRCODE_AVAILABLE: + from PIL import Image, ImageDraw + img = Image.new('RGB', (200, 200), color='white') + draw = ImageDraw.Draw(img) + draw.text((20, 80), "QR Code", fill='black') + draw.text((20, 100), "not available", fill='black') + img_io = BytesIO() + img.save(img_io, 'PNG') + img_io.seek(0) + return send_file(img_io, mimetype='image/png') + + result_url = request.host_url.rstrip('/') + f'/api/result/{result_id}' + + try: + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=4, + ) + qr.add_data(result_url) + qr.make(fit=True) + + img = qr.make_image(fill_color=(218, 165, 32), back_color=(18, 25, 45)) + + img_io = BytesIO() + img.save(img_io, 'PNG') + img_io.seek(0) + + return send_file(img_io, mimetype='image/png') + except Exception as e: + print(f"QR Code generation error: {e}") + from PIL import Image, ImageDraw + img = Image.new('RGB', (200, 200), color='white') + draw = ImageDraw.Draw(img) + draw.text((10, 80), "QR Error", fill='black') + draw.text((10, 100), result_url[:30], fill='black') + img_io = BytesIO() + img.save(img_io, 'PNG') + img_io.seek(0) + return send_file(img_io, mimetype='image/png') + + if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..2f2e05a --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,749 @@ + + + + + + Админ-панель - Управление вопросами викторины + + + +
+
+

📝 Админ-панель управления вопросами

+
+ + + + + 🎮 Перейти в викторину +
+
+ +
+
+
0
+
Всего вопросов
+
+
+
0
+
Активных игр
+
+
+ + + +
+
Загрузка вопросов...
+
+
+ + + + + + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 83ff488..eae2f94 100644 --- a/templates/index.html +++ b/templates/index.html @@ -101,12 +101,41 @@ color: #666; } + /* Вопрос с возможностью подсветки */ + .question-wrapper { + margin: 20px 30px; + padding: 20px; + border-radius: 15px; + transition: all 0.3s ease; + position: relative; + } + + .question-wrapper.correct-highlight { + background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%); + border: 2px solid #28a745; + box-shadow: 0 0 15px rgba(40, 167, 69, 0.3); + } + + .question-wrapper.wrong-highlight { + background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%); + border: 2px solid #dc3545; + box-shadow: 0 0 15px rgba(220, 53, 69, 0.3); + } + .question-text { - padding: 30px 30px 20px; font-size: 24px; font-weight: 600; color: #333; line-height: 1.4; + margin-bottom: 10px; + } + + .question-wrapper.correct-highlight .question-text { + color: #155724; + } + + .question-wrapper.wrong-highlight .question-text { + color: #721c24; } .options { @@ -152,7 +181,7 @@ opacity: 0.7; } - .next-btn, .restart-btn { + .next-btn { margin: 10px 30px 30px; padding: 15px; font-size: 18px; @@ -162,9 +191,6 @@ cursor: pointer; transition: all 0.3s ease; width: calc(100% - 60px); - } - - .next-btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; } @@ -215,9 +241,6 @@ } .result-buttons { - display: flex; - flex-direction: column; - gap: 15px; margin: 20px 0 30px; } @@ -230,56 +253,52 @@ cursor: pointer; transition: all 0.3s ease; width: 100%; - } - - .btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; } - .btn-secondary { - background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); - color: white; - } - - .btn-qr { - background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); - color: white; - } - - .btn:hover { + .result-buttons .btn:hover { transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); + box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); } .qr-container { - margin: 20px 0; - display: none; + margin: 30px 0; + padding: 20px; + background: #f8f9fa; + border-radius: 15px; text-align: center; } - .qr-container.show { - display: block; - } - .qr-code { display: inline-block; - padding: 20px; + padding: 15px; background: white; border-radius: 15px; box-shadow: 0 5px 15px rgba(0,0,0,0.1); + margin-bottom: 15px; } .qr-code img { - max-width: 200px; + max-width: 180px; height: auto; + display: block; } .qr-link { - margin-top: 10px; font-size: 12px; color: #666; word-break: break-all; + margin-top: 10px; + } + + .qr-link a { + color: #667eea; + text-decoration: none; + } + + .qr-link a:hover { + text-decoration: underline; } .questions-review { @@ -371,7 +390,6 @@ @media (max-width: 600px) { .question-text { font-size: 18px; - padding: 20px; } .option-btn { @@ -395,6 +413,15 @@ .result-grade { font-size: 24px; } + + .qr-code img { + max-width: 150px; + } + + .question-wrapper { + margin: 15px 15px; + padding: 15px; + } } @@ -419,8 +446,10 @@
⏱️ 10 секунд
-
- Загрузка вопроса... +
+
+ Загрузка вопроса... +
@@ -444,19 +473,15 @@
-
- - +
-
-
+
QR-код с результатом
-
@@ -480,6 +505,7 @@ const timerProgress = document.getElementById('timerProgress'); const timerText = document.getElementById('timerText'); const questionText = document.getElementById('questionText'); + const questionWrapper = document.getElementById('questionWrapper'); const optionsContainer = document.getElementById('optionsContainer'); const feedback = document.getElementById('feedback'); const nextBtn = document.getElementById('nextBtn'); @@ -490,10 +516,8 @@ const qrImage = document.getElementById('qrImage'); const qrLink = document.getElementById('qrLink'); - // Опции кнопок const optionBtns = document.querySelectorAll('.option-btn'); - // Начать новую игру async function startNewGame() { try { const response = await fetch('/api/new_game', { @@ -513,9 +537,10 @@ questionArea.classList.remove('hidden'); resultArea.classList.add('hidden'); questionsReview.classList.add('hidden'); - qrContainer.classList.remove('show'); isAnswered = false; + removeQuestionHighlight(); + startTimer(10); } catch (error) { console.error('Ошибка:', error); @@ -523,7 +548,20 @@ } } - // Отображение вопроса + function highlightQuestion(isCorrect) { + removeQuestionHighlight(); + if (isCorrect) { + questionWrapper.classList.add('correct-highlight'); + } else { + questionWrapper.classList.add('wrong-highlight'); + } + } + + function removeQuestionHighlight() { + questionWrapper.classList.remove('correct-highlight'); + questionWrapper.classList.remove('wrong-highlight'); + } + function displayQuestion(question, qNumber) { if (!question) return; @@ -531,7 +569,6 @@ questionCounter.textContent = `Вопрос ${qNumber}/${totalQuestions}`; questionText.textContent = question.text; - // Обновляем варианты ответов question.options.forEach((option, index) => { if (optionBtns[index]) { optionBtns[index].textContent = `${String.fromCharCode(65+index)}. ${option}`; @@ -540,13 +577,12 @@ } }); - // Скрываем фидбек и кнопку далее feedback.classList.add('hidden'); nextBtn.classList.add('hidden'); isAnswered = false; + removeQuestionHighlight(); } - // Таймер function startTimer(seconds) { if (timerInterval) clearInterval(timerInterval); @@ -580,17 +616,16 @@ } } - // Обработка timeout async function handleTimeout() { if (isAnswered) return; isAnswered = true; clearInterval(timerInterval); - // Отключаем кнопки + highlightQuestion(false); + optionBtns.forEach(btn => btn.disabled = true); - // Отправляем пустой ответ (время вышло) try { const response = await fetch('/api/check_answer', { method: 'POST', @@ -618,30 +653,37 @@ } } - // Обработка ответа пользователя async function handleAnswer(answerIndex) { if (isAnswered) return; isAnswered = true; clearInterval(timerInterval); - // Визуальная обратная связь const selectedBtn = optionBtns[answerIndex]; + + // ВАЖНО: проверяем правильность ответа + // currentQuestion.correct приходит от сервера с индексом правильного ответа const isCorrect = (answerIndex === currentQuestion.correct); + console.log('Answer index:', answerIndex); + console.log('Correct index:', currentQuestion.correct); + console.log('Is correct:', isCorrect); + + // Подсвечиваем вопрос + highlightQuestion(isCorrect); + if (isCorrect) { selectedBtn.classList.add('correct'); + currentScore++; + updateScoreDisplay(); } else { selectedBtn.classList.add('wrong'); - // Показываем правильный ответ const correctBtn = optionBtns[currentQuestion.correct]; if (correctBtn) correctBtn.classList.add('correct'); } - // Отключаем все кнопки optionBtns.forEach(btn => btn.disabled = true); - // Отправляем ответ на сервер try { const response = await fetch('/api/check_answer', { method: 'POST', @@ -665,7 +707,6 @@ } } - // Показать фидбек после ответа function showFeedback(result) { const feedbackTitle = document.getElementById('feedbackTitle'); const feedbackText = document.getElementById('feedbackText'); @@ -673,8 +714,6 @@ if (result.is_correct) { feedbackTitle.textContent = '✅ Правильно!'; feedbackTitle.style.color = '#28a745'; - currentScore = result.score; - updateScoreDisplay(); feedbackText.textContent = result.explanation || 'Отличный результат!'; } else { feedbackTitle.textContent = '❌ Неправильно!'; @@ -685,7 +724,6 @@ feedback.classList.remove('hidden'); } - // Загрузить следующий вопрос function loadNextQuestion(data) { if (data.next_question) { displayQuestion(data.next_question, data.question_number); @@ -693,7 +731,6 @@ } } - // Показать все вопросы с ответами function displayAllQuestions(questionsSummary) { questionsReview.innerHTML = '

📋 Детальный разбор всех вопросов

'; questionsReview.classList.remove('hidden'); @@ -728,30 +765,26 @@ }); } - // Показать QR-код - async function showQRCode(resultId) { + async function displayQRCode(resultId) { try { const response = await fetch(`/api/qrcode/${resultId}`); if (response.ok) { const blob = await response.blob(); const url = URL.createObjectURL(blob); qrImage.src = url; - - // Показываем ссылку + + // Ссылка на изображение с результатами const fullUrl = window.location.origin + `/api/result/${resultId}`; - qrLink.innerHTML = `${fullUrl}`; - - qrContainer.classList.add('show'); + qrLink.innerHTML = `📎 Ссылка на результат: ${fullUrl}`; } else { - alert('Не удалось сгенерировать QR-код'); + qrContainer.innerHTML = '

❌ Не удалось сгенерировать QR-код

'; } } catch (error) { console.error('Ошибка:', error); - alert('Ошибка при генерации QR-кода'); + qrContainer.innerHTML = '

❌ Ошибка при генерации QR-кода

'; } } - // Показать результат function showResult(result) { clearInterval(timerInterval); questionArea.classList.add('hidden'); @@ -766,18 +799,17 @@ resultMessage.textContent = result.message; currentResultId = result.result_id; - // Показываем разбор вопросов (после кнопок) + displayQRCode(currentResultId); + if (result.questions_summary) { displayAllQuestions(result.questions_summary); } } - // Обновить отображение счёта function updateScoreDisplay() { scoreDisplay.textContent = `Счёт: ${currentScore}`; } - // Обработчики событий optionBtns.forEach(btn => { btn.addEventListener('click', () => { const index = parseInt(btn.dataset.index); @@ -789,19 +821,6 @@ startNewGame(); }); - document.getElementById('showQrBtn').addEventListener('click', () => { - if (currentResultId) { - showQRCode(currentResultId); - } else { - alert('Результаты ещё не готовы'); - } - }); - - document.getElementById('hideQrBtn').addEventListener('click', () => { - qrContainer.classList.remove('show'); - }); - - // Запуск игры startNewGame();