Files
qivip/django/templates/play/lobby.html

375 lines
17 KiB
HTML

{% extends 'base.html' %}
{% block content %}
<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>
{% if request.session.mode != "learn" %}
<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>
{% endif %}
<!-- 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>
<a href="{% url 'play:create_participant' quiz_game.join_code %}">Auftreten ändern...</a>
</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>
</div>
{% endblock %}
{% block extra_js %}
<script>
const joinCode = '{{ quiz_game.join_code }}';
const wsScheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
let lobbySocket = null;
let reconnectAttempts = 0;
let pingInterval = null;
const maxReconnectAttempts = 5;
const pingDelay = 30000; // 30 seconds
const heartbeatDelay = 10000; // 10 seconds
let heartbeatInterval = null;
function startPingInterval() {
stopPingInterval();
pingInterval = setInterval(() => {
if (lobbySocket && lobbySocket.readyState === WebSocket.OPEN) {
lobbySocket.send(JSON.stringify({ type: 'ping' }));
}
}, pingDelay);
}
function stopPingInterval() {
if (pingInterval) {
clearInterval(pingInterval);
pingInterval = null;
}
}
function startHeartbeat() {
stopHeartbeat();
heartbeatInterval = setInterval(() => {
if (lobbySocket && lobbySocket.readyState === WebSocket.OPEN) {
lobbySocket.send(JSON.stringify({
type: 'heartbeat',
participant_id: '{{ participant.participant_id }}'
}));
}
}, heartbeatDelay);
}
function stopHeartbeat() {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = 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();
{% if participant %}
startHeartbeat();
{% endif %}
// Request initial participants list
lobbySocket.send(JSON.stringify({
type: 'update_participants'
}));
};
lobbySocket.onclose = function(e) {
wsUtil.handleClose(e);
stopPingInterval();
stopHeartbeat();
lobbySocket = null;
reconnectAttempts++;
if (reconnectAttempts < maxReconnectAttempts) {
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 10000); // Exponential backoff, max 10s
setTimeout(connectWebSocket, delay);
}
};
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;
}
}
// 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 (!list) {
console.warn('Participants list element not found');
return;
}
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>';
}
}
{% 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 %}