Initial Game Logic 2
This commit is contained in:
@@ -1,35 +1,129 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="{% static 'css/t-style.css' %}">
|
||||
<title>qivip</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<div class="qp-question-container">
|
||||
<p class="text-gray-700 font-bold text-xl uppercase">Frage X</p>
|
||||
<img src="integral.png">
|
||||
<h1>Frage: {{ question.data.question }}</h1>
|
||||
<div>
|
||||
<div class="p-4">
|
||||
<p class="text-blue-500 text-xl">Verbleibende Zeit:</p>
|
||||
<p id="remaining_time" class="text-6xl">36</p>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<p class="text-blue-500 text-xl">7/14 Antworten</p>
|
||||
</div>
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<p class="text-gray-700 font-bold text-xl mb-4">Frage {{ question_index|add:1 }} von {{ total_questions }}</p>
|
||||
<h1 class="text-3xl font-bold mb-6">{{ question_data.question }}</h1>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mb-6">
|
||||
<div class="p-4 bg-blue-50 rounded-lg">
|
||||
<p class="text-blue-600 text-xl mb-2">Verbleibende Zeit</p>
|
||||
<p id="remaining-time" class="text-6xl font-bold text-blue-700">30</p>
|
||||
</div>
|
||||
<div class="p-4 bg-blue-50 rounded-lg">
|
||||
<p class="text-blue-600 text-xl mb-2">Antworten</p>
|
||||
<p id="answer-count" class="text-6xl font-bold text-blue-700">0/0</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="qp-answer-container-host" id="qp-answer-container-host">
|
||||
{% for option in question.data.options %}
|
||||
<div>
|
||||
<p>{{ option.value }}</p>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{% for option in question_data.options %}
|
||||
<div class="p-4 bg-gray-100 rounded-lg" data-correct="{{ option.is_correct }}">
|
||||
<p class="text-lg">{{ option.value }}</p>
|
||||
<p id="option-count-{{ forloop.counter0 }}" class="text-gray-600 hidden">0 Antworten</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% include 'play/game/common_scripts.html' %}
|
||||
<script>
|
||||
const joinCode = '{{ quiz_game.join_code }}';
|
||||
const hostId = '{{ host_id }}';
|
||||
const questionStartTime = {{ start_time }};
|
||||
const questionDuration = 30000; // 30 seconds in milliseconds
|
||||
const gameSocket = initializeGameSocket(joinCode);
|
||||
let answeredParticipants = 0;
|
||||
let totalParticipants = 0;
|
||||
const optionCounts = {};
|
||||
|
||||
gameSocket.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'participant_answer') {
|
||||
answeredParticipants++;
|
||||
updateAnswerCount();
|
||||
updateOptionCount(data.answer);
|
||||
} else if (data.type === 'participant_list_update') {
|
||||
totalParticipants = data.participants.length;
|
||||
updateAnswerCount();
|
||||
} else if (data.type === 'game_state_update' &&
|
||||
data.action === 'show_scores' &&
|
||||
data.redirect_url) {
|
||||
window.location.href = data.redirect_url;
|
||||
}
|
||||
};
|
||||
|
||||
gameSocket.onclose = function(e) {
|
||||
wsUtil.handleClose(e);
|
||||
};
|
||||
|
||||
function updateAnswerCount() {
|
||||
const answerCountElement = document.getElementById('answer-count');
|
||||
if (answerCountElement) {
|
||||
answerCountElement.textContent = `${answeredParticipants}/${totalParticipants}`;
|
||||
}
|
||||
|
||||
// If everyone has answered, advance to scores
|
||||
if (answeredParticipants === totalParticipants && totalParticipants > 0) {
|
||||
advanceToScores();
|
||||
}
|
||||
}
|
||||
|
||||
function updateOptionCount(optionIndex) {
|
||||
optionCounts[optionIndex] = (optionCounts[optionIndex] || 0) + 1;
|
||||
const optionElement = document.getElementById(`option-count-${optionIndex}`);
|
||||
if (optionElement) {
|
||||
optionElement.textContent = `${optionCounts[optionIndex]} Antworten`;
|
||||
}
|
||||
}
|
||||
|
||||
function advanceToScores() {
|
||||
// Show correct answers and answer counts
|
||||
var correctOptions = document.querySelectorAll('[data-correct="True"]');
|
||||
for (var i = 0; i < correctOptions.length; i++) {
|
||||
correctOptions[i].classList.add('border-2', 'border-green-500');
|
||||
}
|
||||
|
||||
// Show answer counts
|
||||
var countElements = document.querySelectorAll('[id^="option-count-"]');
|
||||
for (var j = 0; j < countElements.length; j++) {
|
||||
countElements[j].classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Wait a moment to show the correct answer and counts
|
||||
setTimeout(function() {
|
||||
gameSocket.send(JSON.stringify({
|
||||
'type': 'advance_to_scores',
|
||||
'host_id': hostId
|
||||
}));
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Timer
|
||||
function updateTimer() {
|
||||
const now = Date.now();
|
||||
const elapsed = now - questionStartTime;
|
||||
const remaining = Math.max(0, Math.ceil((questionDuration - elapsed) / 1000));
|
||||
|
||||
document.getElementById('remaining-time').textContent = remaining;
|
||||
|
||||
if (remaining === 0) {
|
||||
advanceToScores();
|
||||
} else {
|
||||
requestAnimationFrame(updateTimer);
|
||||
}
|
||||
}
|
||||
|
||||
// Request initial participants list
|
||||
gameSocket.onopen = function(e) {
|
||||
gameSocket.send(JSON.stringify({
|
||||
'type': 'update_participants'
|
||||
}));
|
||||
updateTimer();
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,19 +1,115 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="{% static 'css/t-style.css' %}">
|
||||
<title>qivip</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="qp-answer-container" id="qp-answer-container">
|
||||
{% for option in question.data.options %}
|
||||
<div>
|
||||
<p>{{ option.value }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-end mb-4">
|
||||
<button onclick="leaveGame()" class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">
|
||||
Spiel verlassen
|
||||
</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<p class="text-gray-700 font-bold text-xl mb-4">Frage {{ question_index|add:1 }} von {{ total_questions }}</p>
|
||||
<h1 class="text-3xl font-bold mb-6">{{ question_data.question }}</h1>
|
||||
|
||||
<div class="p-4 bg-blue-50 rounded-lg mb-6">
|
||||
<p class="text-blue-600 text-xl mb-2">Verbleibende Zeit</p>
|
||||
<p id="remaining-time" class="text-6xl font-bold text-blue-700">30</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4" id="options-container">
|
||||
{% for option in question_data.options %}
|
||||
<button
|
||||
class="p-4 bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 answer-option"
|
||||
data-index="{{ forloop.counter0 }}"
|
||||
onclick="submitAnswer({{ forloop.counter0 }});">
|
||||
<p class="text-lg">{{ option.value }}</p>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% include 'play/game/common_scripts.html' %}
|
||||
<script>
|
||||
let hasAnswered = false;
|
||||
const joinCode = '{{ quiz_game.join_code }}';
|
||||
const participantId = '{{ participant.participant_id }}';
|
||||
const questionStartTime = {{ start_time }};
|
||||
const questionDuration = 30000; // 30 seconds in milliseconds
|
||||
const gameSocket = initializeGameSocket(joinCode);
|
||||
|
||||
gameSocket.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'redirect' && data.url) {
|
||||
window.location.href = data.url;
|
||||
} else if (data.type === 'game_state_update' &&
|
||||
data.action === 'show_scores' &&
|
||||
data.redirect_url) {
|
||||
window.location.href = data.redirect_url;
|
||||
}
|
||||
};
|
||||
|
||||
gameSocket.onclose = function(e) {
|
||||
wsUtil.handleClose(e);
|
||||
};
|
||||
|
||||
function submitAnswer(optionIndex) {
|
||||
if (hasAnswered) return;
|
||||
|
||||
hasAnswered = true;
|
||||
const now = Date.now();
|
||||
const timeRemaining = Math.max(0, questionDuration - (now - questionStartTime));
|
||||
|
||||
// Disable all options and highlight selected
|
||||
const options = document.getElementsByClassName('answer-option');
|
||||
for (let option of options) {
|
||||
option.disabled = true;
|
||||
option.classList.remove('hover:bg-blue-100');
|
||||
if (parseInt(option.dataset.index) === optionIndex) {
|
||||
option.classList.remove('bg-gray-100');
|
||||
option.classList.add('bg-blue-600', 'text-white');
|
||||
} else {
|
||||
option.classList.add('opacity-50');
|
||||
}
|
||||
}
|
||||
|
||||
// Send answer to server
|
||||
gameSocket.send(JSON.stringify({
|
||||
type: 'submit_answer',
|
||||
participant_id: participantId,
|
||||
answer: optionIndex,
|
||||
time_remaining: timeRemaining
|
||||
}));
|
||||
}
|
||||
|
||||
// Timer
|
||||
function updateTimer() {
|
||||
const now = Date.now();
|
||||
const elapsed = now - questionStartTime;
|
||||
const remaining = Math.max(0, Math.ceil((questionDuration - elapsed) / 1000));
|
||||
|
||||
document.getElementById('remaining-time').textContent = remaining;
|
||||
|
||||
if (remaining === 0 && !hasAnswered) {
|
||||
// Auto-submit timeout answer
|
||||
submitAnswer(-1);
|
||||
} else if (remaining > 0) {
|
||||
requestAnimationFrame(updateTimer);
|
||||
}
|
||||
}
|
||||
|
||||
function leaveGame() {
|
||||
if (confirm('Möchtest du das Spiel wirklich verlassen?')) {
|
||||
gameSocket.send(JSON.stringify({
|
||||
type: 'leave_game',
|
||||
participant_id: participantId
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Start timer when page loads
|
||||
updateTimer();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,5 +1,128 @@
|
||||
<!-->
|
||||
Spieler sehen ihre Punktzahl, Platzierung und Richtig/Falsch
|
||||
{% extends 'base.html' %}
|
||||
|
||||
Host: Scoreboard, Button: 'Weiter'
|
||||
</!-->
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<h1 class="text-3xl font-bold mb-6">Ergebnisse</h1>
|
||||
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-bold mb-4">Aktuelle Frage</h2>
|
||||
<p class="text-lg mb-2">{{ question_data.question }}</p>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{% for option in question_data.options %}
|
||||
<div class="p-4 rounded-lg {% if option.is_correct %}bg-green-100 border-2 border-green-500{% else %}bg-gray-100{% endif %}">
|
||||
<p class="text-lg">{{ option.value }}</p>
|
||||
<p class="text-gray-600" id="option-count-{{ forloop.counter0 }}">0 Antworten</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-bold mb-4">Punktestand</h2>
|
||||
<div class="space-y-2">
|
||||
{% for participant in participants %}
|
||||
<div class="p-4 {% if forloop.first %}bg-yellow-100 border-2 border-yellow-500{% else %}bg-gray-100{% endif %} rounded-lg flex justify-between items-center">
|
||||
<div>
|
||||
<p class="text-lg font-bold">{{ participant.display_name }}</p>
|
||||
<p class="text-gray-600">{{ participant.score }} Punkte</p>
|
||||
</div>
|
||||
{% if participant.last_answer_correct %}
|
||||
<span class="text-green-500">✓</span>
|
||||
{% elif participant.last_answer_correct == False %}
|
||||
<span class="text-red-500">✗</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_host %}
|
||||
{% if is_last_question %}
|
||||
<button id="finish-button" class="w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
|
||||
Quiz beenden
|
||||
</button>
|
||||
{% else %}
|
||||
<button id="next-button" class="w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
|
||||
Nächste Frage
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% include 'play/game/common_scripts.html' %}
|
||||
<script>
|
||||
const joinCode = '{{ quiz_game.join_code }}';
|
||||
const hostId = '{{ host_id }}';
|
||||
const wsScheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const gameSocket = new WebSocket(
|
||||
`${wsScheme}://${window.location.host}/ws/game/${joinCode}/`
|
||||
);
|
||||
|
||||
gameSocket.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'game_state_update' &&
|
||||
data.redirect_url &&
|
||||
(data.action === 'next_question' || data.action === 'finish_game')) {
|
||||
window.location.href = data.redirect_url;
|
||||
} else if (data.type === 'answer_stats') {
|
||||
updateAnswerStats(data.stats);
|
||||
}
|
||||
};
|
||||
|
||||
gameSocket.onclose = function(e) {
|
||||
// Normal closure (code 1000) oder Navigation zu einer anderen Seite (code 1001)
|
||||
if (e.code === 1000 || e.code === 1001) {
|
||||
console.log('Game socket closed normally');
|
||||
} else {
|
||||
console.error('Game socket closed unexpectedly', e.code);
|
||||
}
|
||||
};
|
||||
|
||||
{% if is_host %}
|
||||
// Add click handlers for host buttons
|
||||
{% if is_last_question %}
|
||||
var finishButton = document.getElementById('finish-button');
|
||||
if (finishButton) {
|
||||
finishButton.addEventListener('click', function() {
|
||||
gameSocket.send(JSON.stringify({
|
||||
'type': 'finish_game',
|
||||
'host_id': hostId
|
||||
}));
|
||||
});
|
||||
}
|
||||
{% else %}
|
||||
var nextButton = document.getElementById('next-button');
|
||||
if (nextButton) {
|
||||
nextButton.addEventListener('click', function() {
|
||||
gameSocket.send(JSON.stringify({
|
||||
'type': 'next_question',
|
||||
'host_id': hostId
|
||||
}));
|
||||
});
|
||||
}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
function updateAnswerStats(stats) {
|
||||
for (var optionIndex in stats) {
|
||||
if (stats.hasOwnProperty(optionIndex)) {
|
||||
var element = document.getElementById('option-count-' + optionIndex);
|
||||
if (element) {
|
||||
element.textContent = stats[optionIndex] + ' Antworten';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Request answer stats when page loads
|
||||
gameSocket.onopen = function(e) {
|
||||
gameSocket.send(JSON.stringify({
|
||||
'type': 'get_answer_stats'
|
||||
}));
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,5 +1,53 @@
|
||||
<!-->
|
||||
Spieler: Countdown, automatische Weiterleitung
|
||||
{% extends 'base.html' %}
|
||||
|
||||
(Host: Frage)
|
||||
</!-->
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-6 text-center">
|
||||
<h1 class="text-3xl font-bold mb-6">Warte auf andere Spieler...</h1>
|
||||
|
||||
<div class="p-4 bg-blue-50 rounded-lg mb-6 inline-block">
|
||||
<p class="text-blue-600 text-xl mb-2">Die nächste Frage beginnt in</p>
|
||||
<p id="countdown" class="text-6xl font-bold text-blue-700">5</p>
|
||||
</div>
|
||||
|
||||
<div class="animate-pulse">
|
||||
<p class="text-gray-600">Bitte warte, bis alle Spieler bereit sind</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% include 'play/game/common_scripts.html' %}
|
||||
<script>
|
||||
const joinCode = '{{ quiz_game.join_code }}';
|
||||
const wsScheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const gameSocket = new WebSocket(
|
||||
wsScheme + '://' + window.location.host + '/ws/game/' + joinCode + '/'
|
||||
);
|
||||
|
||||
let countdown = 5;
|
||||
const countdownElement = document.getElementById('countdown');
|
||||
|
||||
gameSocket.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'game_state_update' && data.action === 'start_question' && data.redirect_url) {
|
||||
window.location.href = data.redirect_url;
|
||||
}
|
||||
};
|
||||
|
||||
gameSocket.onclose = function(e) {
|
||||
wsUtil.handleClose(e);
|
||||
};
|
||||
|
||||
function updateCountdown() {
|
||||
countdownElement.textContent = countdown;
|
||||
if (countdown > 0) {
|
||||
countdown--;
|
||||
setTimeout(updateCountdown, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
updateCountdown();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,13 +1,63 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<h1>User for a Game:</h1>
|
||||
<div class="input-group border-blue-600 border-2 rounded-xl shadow-md">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="join_code" value="{{join_code}}">
|
||||
<input type="text" name="display_name" placeholder="Anzeigename">
|
||||
<button type="submit">Speichern</button>
|
||||
</form>
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="max-w-md mx-auto bg-white rounded-lg shadow-lg p-6">
|
||||
<h1 class="text-2xl font-bold mb-6 text-center text-blue-600">Spieler einrichten</h1>
|
||||
|
||||
<div class="text-center mb-6">
|
||||
<div class="inline-block bg-blue-100 rounded-lg px-4 py-2">
|
||||
<span class="text-sm text-blue-800">Spiel-Code:</span>
|
||||
<span class="font-mono font-bold text-blue-900">{{ join_code }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" class="space-y-4">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="join_code" value="{{join_code}}">
|
||||
|
||||
<div class="bg-gray-50 p-4 rounded-lg mb-4">
|
||||
<p class="text-sm text-gray-600 mb-2">Wähle einen Anzeigenamen für das Spiel:</p>
|
||||
<input type="text"
|
||||
name="display_name"
|
||||
required
|
||||
maxlength="20"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Dein Spielername">
|
||||
<p class="text-xs text-gray-500 mt-2">Maximal 20 Zeichen</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center space-x-4">
|
||||
<a href="/"
|
||||
class="px-6 py-2 bg-gray-200 hover:bg-gray-300 rounded-lg transition-colors duration-200">
|
||||
Abbrechen
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors duration-200">
|
||||
Spiel beitreten
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if error %}
|
||||
<div class="mt-4 p-4 bg-red-100 border-l-4 border-red-500 text-red-700">
|
||||
<p class="font-medium">Fehler</p>
|
||||
<p class="text-sm">{{ error }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-height: calc(100vh - 4rem);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -1,23 +1,62 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<h3>{{ quiz.name }}</h3>
|
||||
<h1 class="text-center text-4xl font-bold mb-4">{{ quiz_game.join_code }}</h1>
|
||||
{% if participant %}
|
||||
<div class="mt-4 mb-4 text-center">
|
||||
<h3>Sichtbar als <b>{{ participant.display_name }}</b></h3>
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
|
||||
<div class="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-6">
|
||||
<!-- Quiz Info -->
|
||||
<div class="text-center mb-8">
|
||||
<h3 class="text-xl text-gray-600 mb-2">{{ quiz.name }}</h3>
|
||||
<div class="bg-blue-100 rounded-lg p-4 mb-4">
|
||||
<h1 class="text-4xl font-bold text-blue-800">{{ quiz_game.join_code }}</h1>
|
||||
<p class="text-sm text-blue-600 mt-1">Spiel-Code</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Participant Info -->
|
||||
{% if participant %}
|
||||
<div class="mb-6 text-center">
|
||||
<div class="inline-flex items-center bg-green-100 px-4 py-2 rounded-full">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
|
||||
<span>Angemeldet als <b>{{ participant.display_name }}</b></span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Participants List -->
|
||||
<div class="mb-6">
|
||||
<h3 class="font-bold text-lg mb-3 flex items-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
Teilnehmer
|
||||
</h3>
|
||||
<ul id="participants-list" class="space-y-2 max-h-60 overflow-y-auto">
|
||||
<li class="text-gray-500 p-3 bg-gray-50 rounded-lg text-center">Warte auf Teilnehmer...</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="space-y-3">
|
||||
{% if host_id %}
|
||||
<button id="start-button" class="w-full p-4 rounded-lg bg-blue-600 text-white font-bold hover:bg-blue-700 transform hover:scale-105 transition duration-200 shadow-md flex items-center justify-center" type="button">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Spiel starten
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if participant %}
|
||||
<button id="leave-button" class="w-full p-4 rounded-lg bg-red-100 text-red-700 font-bold hover:bg-red-200 transition duration-200 flex items-center justify-center" type="button">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
Spiel verlassen
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mb-4">
|
||||
<h3 class="font-bold mb-2">Teilnehmer:</h3>
|
||||
<ul id="participants-list" class="space-y-2">
|
||||
<li class="text-gray-500">Warte auf Teilnehmer...</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% if host_id %}
|
||||
<button id="start-button" class="w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200" type="button">Starten</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -25,50 +64,296 @@
|
||||
<script>
|
||||
const joinCode = '{{ quiz_game.join_code }}';
|
||||
const wsScheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const lobbySocket = new WebSocket(
|
||||
wsScheme + '://' + window.location.host + '/ws/game/' + joinCode + '/'
|
||||
);
|
||||
let lobbySocket = null;
|
||||
let reconnectAttempts = 0;
|
||||
let pingInterval = null;
|
||||
const maxReconnectAttempts = 5;
|
||||
const pingDelay = 30000; // 30 seconds
|
||||
const heartbeatDelay = 10000; // 10 seconds
|
||||
|
||||
lobbySocket.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'participant_list' || data.type === 'participant_list_update') {
|
||||
updateParticipantsList(data.participants);
|
||||
function startPingInterval() {
|
||||
if (pingInterval) {
|
||||
clearInterval(pingInterval);
|
||||
}
|
||||
};
|
||||
pingInterval = setInterval(() => {
|
||||
if (lobbySocket && lobbySocket.readyState === WebSocket.OPEN) {
|
||||
lobbySocket.send(JSON.stringify({ type: 'ping' }));
|
||||
}
|
||||
}, pingDelay);
|
||||
}
|
||||
|
||||
lobbySocket.onclose = function(e) {
|
||||
console.error('Lobby socket closed unexpectedly');
|
||||
};
|
||||
function stopPingInterval() {
|
||||
if (pingInterval) {
|
||||
clearInterval(pingInterval);
|
||||
pingInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function connectWebSocket() {
|
||||
if (lobbySocket && (lobbySocket.readyState === WebSocket.CONNECTING || lobbySocket.readyState === WebSocket.OPEN)) {
|
||||
return lobbySocket; // Already connected or connecting
|
||||
}
|
||||
|
||||
if (reconnectAttempts >= maxReconnectAttempts) {
|
||||
console.error('Max reconnection attempts reached');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
lobbySocket = new WebSocket(
|
||||
wsScheme + '://' + window.location.host + '/ws/play/lobby/' + joinCode + '/'
|
||||
);
|
||||
|
||||
lobbySocket.onopen = function(e) {
|
||||
console.log('Lobby socket connected');
|
||||
reconnectAttempts = 0;
|
||||
startPingInterval();
|
||||
// Request initial participants list
|
||||
lobbySocket.send(JSON.stringify({
|
||||
type: 'update_participants'
|
||||
}));
|
||||
};
|
||||
|
||||
lobbySocket.onclose = function(e) {
|
||||
wsUtil.handleClose(e);
|
||||
stopPingInterval();
|
||||
lobbySocket = null;
|
||||
reconnectAttempts++;
|
||||
if (reconnectAttempts < maxReconnectAttempts) {
|
||||
setTimeout(connectWebSocket, 1000 * Math.min(reconnectAttempts, 3));
|
||||
}
|
||||
};
|
||||
|
||||
lobbySocket.onerror = function(e) {
|
||||
console.error('Lobby socket error:', e);
|
||||
};
|
||||
|
||||
lobbySocket.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'participants_list_update') {
|
||||
updateParticipantsList(data.participants);
|
||||
} else if (data.type === 'game_state_update' &&
|
||||
data.action === 'start_game' &&
|
||||
data.redirect_url) {
|
||||
window.location.href = data.redirect_url;
|
||||
} else if (data.type === 'pong') {
|
||||
console.log('Received pong from server');
|
||||
} else if (data.type === 'redirect' && data.url) {
|
||||
window.location.href = data.url;
|
||||
} else if (data.type === 'player_joined' && '{{ host_id }}') {
|
||||
toast.create(`${data.player_name} ist beigetreten`, 'success');
|
||||
} else if (data.type === 'player_left') {
|
||||
if (data.was_kicked) {
|
||||
// Show kick message
|
||||
toast.create(`${data.player_name} wurde vom Host entfernt`, 'error');
|
||||
|
||||
// If this client was kicked, redirect to home
|
||||
if (data.participant_id === '{{ participant.participant_id }}') {
|
||||
window.location.href = '/';
|
||||
}
|
||||
} else {
|
||||
toast.create(`${data.player_name} hat das Spiel verlassen`, 'info');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return lobbySocket;
|
||||
} catch (error) {
|
||||
console.error('Error creating WebSocket:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
{% if participant %}
|
||||
// Send heartbeat every 10 seconds
|
||||
setInterval(() => {
|
||||
if (lobbySocket.readyState === WebSocket.OPEN) {
|
||||
setInterval(function() {
|
||||
if (lobbySocket && lobbySocket.readyState === WebSocket.OPEN) {
|
||||
lobbySocket.send(JSON.stringify({
|
||||
'type': 'heartbeat',
|
||||
'participant_id': '{{ participant.participant_id }}'
|
||||
type: 'heartbeat',
|
||||
participant_id: '{{ participant.participant_id }}'
|
||||
}));
|
||||
}
|
||||
}, 10000);
|
||||
}, heartbeatDelay);
|
||||
{% endif %}
|
||||
|
||||
// Initial connection
|
||||
lobbySocket = connectWebSocket();
|
||||
|
||||
function leaveGame() {
|
||||
if (confirm('Möchtest du das Spiel wirklich verlassen?')) {
|
||||
lobbySocket.send(JSON.stringify({
|
||||
type: 'leave_game',
|
||||
participant_id: '{{ participant.participant_id }}'
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function kickPlayer(participantId) {
|
||||
const button = event.currentTarget;
|
||||
if (!button.disabled && confirm('Möchtest du diesen Spieler wirklich aus dem Spiel entfernen?')) {
|
||||
console.log('Attempting to kick player:', participantId);
|
||||
console.log('Host ID:', '{{ quiz_game.host_id }}');
|
||||
console.log('Current host cookie:', document.cookie.split('; ').find(row => row.startsWith('host_id=')));
|
||||
|
||||
button.disabled = true;
|
||||
button.classList.add('opacity-50');
|
||||
button.innerHTML = '<svg class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">' +
|
||||
'<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>' +
|
||||
'<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>' +
|
||||
'</svg>';
|
||||
|
||||
if (lobbySocket && lobbySocket.readyState === WebSocket.OPEN) {
|
||||
const message = {
|
||||
type: 'kick_player',
|
||||
participant_id: participantId,
|
||||
host_id: '{{ quiz_game.host_id }}'
|
||||
};
|
||||
console.log('Sending kick message:', message);
|
||||
lobbySocket.send(JSON.stringify(message));
|
||||
} else {
|
||||
console.error('WebSocket is not connected');
|
||||
button.disabled = false;
|
||||
button.classList.remove('opacity-50');
|
||||
alert('Verbindungsfehler. Bitte versuche es erneut.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateParticipantsList(participants) {
|
||||
const list = document.getElementById('participants-list');
|
||||
if (participants.length === 0) {
|
||||
list.innerHTML = '<li class="text-gray-500">Noch keine Teilnehmer...</li>';
|
||||
if (!list) {
|
||||
console.warn('Participants list element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = participants
|
||||
.map(p => `<li class="p-2 bg-gray-100 rounded">${p.name}</li>`)
|
||||
.join('');
|
||||
if (!participants || participants.length === 0) {
|
||||
list.innerHTML = '<li class="text-gray-500 p-3 bg-gray-50 rounded-lg text-center">Noch keine Teilnehmer...</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Finde neue und entfernte Teilnehmer
|
||||
const currentParticipants = Array.from(list.children)
|
||||
.map(li => li.textContent)
|
||||
.filter(name => name !== 'Noch keine Teilnehmer...' && name !== 'Fehler beim Aktualisieren der Liste');
|
||||
|
||||
const newParticipants = participants
|
||||
.filter(p => !currentParticipants.includes(p.display_name))
|
||||
.map(p => p.display_name);
|
||||
|
||||
const removedParticipants = currentParticipants
|
||||
.filter(name => !participants.find(p => p.display_name === name));
|
||||
|
||||
// Aktualisiere die Liste mit animierten Einträgen
|
||||
list.innerHTML = participants
|
||||
.filter(p => p && p.display_name) // Filter out invalid participants
|
||||
.map(function(p) {
|
||||
if (!p || !p.display_name) {
|
||||
console.error('Invalid participant data:', p);
|
||||
return '';
|
||||
}
|
||||
const displayName = p.display_name;
|
||||
const initial = displayName.charAt(0) || '?';
|
||||
const participantId = p.participant_id;
|
||||
|
||||
// Check if this entry is for the host
|
||||
const isHost = participantId === '{{ quiz_game.host_id }}';
|
||||
const isCurrentUserHost = document.cookie.includes('host_id={{ quiz_game.host_id }}');
|
||||
console.log('Participant check:', {
|
||||
participantId,
|
||||
displayName,
|
||||
isHost,
|
||||
isCurrentUserHost,
|
||||
gameHostId: '{{ quiz_game.host_id }}'
|
||||
});
|
||||
|
||||
// Show kick button if current user is host and this is not their own entry
|
||||
let kickButton = '';
|
||||
if (isCurrentUserHost && !isHost && participantId) {
|
||||
kickButton = '<button onclick="kickPlayer(\'' + p.participant_id + '\')" ' +
|
||||
'class="p-1.5 rounded-full text-red-600 hover:text-white hover:bg-red-600 ' +
|
||||
'transition-all duration-200 group relative" title="Spieler kicken">' +
|
||||
'<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">' +
|
||||
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' +
|
||||
'd="M6 18L18 6M6 6l12 12"/>' +
|
||||
'</svg>' +
|
||||
'<span class="absolute -top-8 left-1/2 transform -translate-x-1/2 px-2 py-1 ' +
|
||||
'bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 ' +
|
||||
'transition-opacity duration-200 whitespace-nowrap">Spieler kicken</span>' +
|
||||
'</button>';
|
||||
}
|
||||
|
||||
return '<li class="p-3 bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition duration-200 flex items-center justify-between animate-fade-in">' +
|
||||
'<div class="flex items-center">' +
|
||||
'<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-3">' +
|
||||
'<span class="text-blue-600 font-bold">' + initial.toUpperCase() + '</span>' +
|
||||
'</div>' +
|
||||
'<span class="font-medium">' + displayName + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="flex items-center space-x-2">' +
|
||||
(isHost ? '<span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full">Host</span>' : '') +
|
||||
'<span class="w-2 h-2 bg-green-500 rounded-full ml-2"></span>' +
|
||||
kickButton +
|
||||
'</div>' +
|
||||
'</li>';
|
||||
})
|
||||
.join('');
|
||||
|
||||
// Benachrichtigungen werden über WebSocket-Events gehandelt
|
||||
} catch (error) {
|
||||
console.error('Error updating participants list:', error);
|
||||
list.innerHTML = '<li class="text-red-500">Fehler beim Aktualisieren der Liste</li>';
|
||||
}
|
||||
}
|
||||
|
||||
// Request initial participants list
|
||||
lobbySocket.onopen = function(e) {
|
||||
lobbySocket.send(JSON.stringify({
|
||||
'type': 'update_participants'
|
||||
}));
|
||||
};
|
||||
{% if host_id %}
|
||||
const startButton = document.getElementById('start-button');
|
||||
if (startButton) {
|
||||
startButton.addEventListener('click', function() {
|
||||
if (lobbySocket && lobbySocket.readyState === WebSocket.OPEN) {
|
||||
startButton.disabled = true;
|
||||
startButton.classList.add('opacity-50');
|
||||
startButton.innerHTML =
|
||||
'<svg class="animate-spin h-5 w-5 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">' +
|
||||
'<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>' +
|
||||
'<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>' +
|
||||
'</svg>' +
|
||||
'Spiel wird gestartet...';
|
||||
lobbySocket.send(JSON.stringify({
|
||||
type: 'start_game',
|
||||
host_id: '{{ host_id }}'
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{% if participant %}
|
||||
const leaveButton = document.getElementById('leave-button');
|
||||
if (leaveButton) {
|
||||
leaveButton.addEventListener('click', function() {
|
||||
if (lobbySocket && lobbySocket.readyState === WebSocket.OPEN) {
|
||||
leaveButton.disabled = true;
|
||||
leaveButton.classList.add('opacity-50');
|
||||
leaveButton.innerHTML =
|
||||
'<svg class="animate-spin h-5 w-5 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">' +
|
||||
'<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>' +
|
||||
'<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>' +
|
||||
'</svg>' +
|
||||
'Verlasse Spiel...';
|
||||
lobbySocket.send(JSON.stringify({
|
||||
type: 'leave_game',
|
||||
participant_id: '{{ participant.participant_id }}'
|
||||
}));
|
||||
|
||||
// Warte kurz und leite dann zur Startseite weiter
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user