Files
quizify/templates/quiz_buzzer_multiplayer.html

674 lines
24 KiB
HTML
Raw 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>
<head>
<title>{{ translations['quiz_title'] }} Buzzer Multiplayer</title>
<script src="https://sdk.scdn.co/spotify-player.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #0a0e27 0%, #1a1f3a 50%, #0f1419 100%);
color: #e0e0e0;
min-height: 100vh;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.quiz-container {
max-width: 1200px;
width: 100%;
background: rgba(25, 30, 45, 0.95);
border-radius: 20px;
padding: 40px;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.scoreboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.player-card {
background: rgba(15, 20, 25, 0.8);
border-radius: 12px;
padding: 20px;
border: 2px solid rgba(29, 185, 84, 0.3);
transition: all 0.3s ease;
}
.player-card.active {
border-color: #1DB954;
box-shadow: 0 0 20px rgba(29, 185, 84, 0.5);
}
.player-card.buzzed {
border-color: #f44336;
box-shadow: 0 0 20px rgba(244, 67, 54, 0.5);
}
.player-name {
font-size: 1.3em;
font-weight: bold;
color: #1DB954;
margin-bottom: 10px;
}
.player-score {
font-size: 2em;
font-weight: bold;
color: #e0e0e0;
}
.buzzer-timer {
text-align: center;
font-size: 4em;
font-weight: bold;
color: #1DB954;
margin: 30px 0;
text-shadow: 0 0 20px rgba(29, 185, 84, 0.5);
}
.buzzer-timer.warning {
color: #FFA500;
animation: pulse 1s infinite;
}
.buzzer-timer.critical {
color: #f44336;
animation: pulse 0.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.05); }
}
.points-display {
text-align: center;
font-size: 2.5em;
font-weight: bold;
margin: 20px 0;
}
.points-display.high {
color: #4CAF50;
}
.points-display.medium {
color: #FFA500;
}
.points-display.low {
color: #f44336;
}
.buzzer-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 15px;
margin: 30px 0;
}
@media (min-width: 768px) {
.buzzer-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 1024px) {
.buzzer-grid {
grid-template-columns: repeat(4, 1fr);
}
}
.player-buzzer {
height: 150px;
border-radius: 20px;
background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
border: 5px solid rgba(244, 67, 54, 0.3);
font-size: 1.5em;
font-weight: bold;
color: white;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 10px 40px rgba(244, 67, 54, 0.5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.player-buzzer:hover:not(.disabled) {
transform: scale(1.05);
box-shadow: 0 15px 50px rgba(244, 67, 54, 0.7);
}
.player-buzzer:active:not(.disabled) {
transform: scale(0.95);
}
.player-buzzer.disabled {
background: linear-gradient(135deg, #666 0%, #444 100%);
border-color: rgba(100, 100, 100, 0.3);
cursor: not-allowed;
opacity: 0.5;
}
.player-buzzer.start {
background: linear-gradient(135deg, #1DB954 0%, #1ed760 100%);
border-color: rgba(29, 185, 84, 0.3);
box-shadow: 0 10px 40px rgba(29, 185, 84, 0.5);
width: 100%;
grid-column: 1 / -1;
}
.player-buzzer.start:hover {
box-shadow: 0 15px 50px rgba(29, 185, 84, 0.7);
}
.answer-section {
display: none;
text-align: center;
margin-top: 30px;
}
.answer-section.active {
display: block;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.btn {
padding: 12px 24px;
margin: 5px;
background: linear-gradient(135deg, #1DB954 0%, #1ed760 100%);
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 1em;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(29, 185, 84, 0.3);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(29, 185, 84, 0.5);
}
.btn-secondary {
background: linear-gradient(135deg, #535353 0%, #6b6b6b 100%);
box-shadow: 0 4px 15px rgba(83, 83, 83, 0.3);
}
.btn-danger {
background: linear-gradient(135deg, #f44336 0%, #e57373 100%);
}
input[type="text"] {
padding: 14px 20px;
width: 100%;
max-width: 400px;
border: 2px solid rgba(29, 185, 84, 0.3);
background: rgba(15, 20, 35, 0.6);
color: #e0e0e0;
border-radius: 25px;
font-size: 16px;
transition: all 0.3s ease;
outline: none;
}
input[type="text"]:focus {
border-color: #1DB954;
box-shadow: 0 0 15px rgba(29, 185, 84, 0.4);
}
.result-container {
margin: 25px 0;
padding: 20px;
border-radius: 15px;
}
.search-results {
position: relative;
max-width: 400px;
margin: 10px auto;
background: rgba(15, 20, 35, 0.95);
border-radius: 12px;
max-height: 300px;
overflow-y: auto;
display: none;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
}
.search-item {
padding: 12px 20px;
cursor: pointer;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
transition: background 0.2s;
}
.search-item:hover {
background: rgba(29, 185, 84, 0.2);
}
.search-item:last-child {
border-bottom: none;
}
.current-player {
text-align: center;
font-size: 1.5em;
color: #1DB954;
margin: 20px 0;
font-weight: bold;
}
</style>
<script>
let allTracks = {{ all_tracks|tojson }};
let currentGameMode = "{{ game_mode }}";
let correctAnswer = "";
const i18n = {{ translations|tojson }};
let buzzTimer = null;
let startTime = null;
let buzzTime = null;
let maxPoints = parseInt(localStorage.getItem('buzzer_max_points')) || 1000;
let gracePeriod = parseInt(localStorage.getItem('buzzer_grace_period')) || 5;
let decayRate = parseInt(localStorage.getItem('buzzer_decay_rate')) || 50;
let gameStarted = false;
let currentBuzzer = null;
// Initialisierung verschoben nach unten
let teamCount = 0;
let players = [];
let playerScoresData = {{ player_scores|tojson }} || [0, 0, 0, 0, 0, 0];
function initializePlayers() {
teamCount = parseInt(localStorage.getItem('team_count')) || 4;
players = [];
for (let i = 1; i <= teamCount; i++) {
const teamName = localStorage.getItem(`team_${i}_name`) || `Team ${i}`;
const scoreIndex = i - 1;
const score = playerScoresData[scoreIndex] || 0;
players.push({ id: i, name: teamName, score: score });
}
console.log('Loaded teams:', players);
console.log('Team count:', teamCount);
}
window.onSpotifyWebPlaybackSDKReady = () => {
const token = '{{ access_token }}';
const player = new Spotify.Player({
name: 'Musik Quiz Buzzer Multiplayer',
getOAuthToken: cb => { cb(token); },
volume: 0.5
});
player.addListener('initialization_error', ({ message }) => { console.error(message); });
player.addListener('authentication_error', ({ message }) => { console.error(message); });
player.addListener('account_error', ({ message }) => { console.error(message); });
player.addListener('playback_error', ({ message }) => { console.error(message); });
player.addListener('ready', ({ device_id }) => {
console.log('Ready with Device ID', device_id);
document.getElementById('device_id').value = device_id;
window.deviceId = device_id;
setCorrectAnswer();
// Aktualisiere Scoreboard falls schon initialisiert
if (players.length > 0) {
updateScoreboard();
}
});
player.addListener('player_state_changed', state => {
if (!state) return;
if (!state.paused && gameStarted && !startTime) {
console.log('Music started playing, starting timer in 1 second');
setTimeout(() => {
startBuzzerTimer();
}, 1000);
}
});
player.addListener('not_ready', ({ device_id }) => {
console.log('Device ID has gone offline', device_id);
});
player.connect();
window.spotifyPlayer = player;
};
function updateScoreboard() {
players.forEach((player, index) => {
const card = document.getElementById(`player${player.id}`);
if (card) {
card.querySelector('.player-score').textContent = player.score;
}
});
}
function startGame() {
if (gameStarted) return;
gameStarted = true;
document.getElementById('startButton').classList.add('disabled');
document.getElementById('startButton').style.cursor = 'wait';
const device_id = window.deviceId;
const startPosition = getOption('startPosition', 'start');
let position_ms = 0;
if (startPosition === 'random') {
const duration = {{ track.duration_ms if track.duration_ms else 180000 }};
position_ms = Math.floor(Math.random() * (duration - 30000));
}
fetch(`/play_track?device_id=${device_id}&track_uri={{ track.uri }}&position_ms=${position_ms}`, { method: 'POST' })
.then(response => response.json())
.then(() => {
console.log('Play command sent');
document.getElementById('startButton').style.display = 'none';
// Aktiviere alle Buzzer
players.forEach(player => {
document.getElementById(`buzzer${player.id}`).classList.remove('disabled');
});
})
.catch(error => console.error('Error starting playback:', error));
}
function startBuzzerTimer() {
startTime = Date.now();
updateTimer();
}
function updateTimer() {
if (currentBuzzer) return;
const elapsed = (Date.now() - startTime) / 1000;
const timerDisplay = document.getElementById('buzzerTimer');
const pointsDisplay = document.getElementById('pointsDisplay');
timerDisplay.textContent = elapsed.toFixed(2) + 's';
const currentPoints = calculatePoints(elapsed);
pointsDisplay.textContent = currentPoints + ' Punkte';
timerDisplay.className = 'buzzer-timer';
pointsDisplay.className = 'points-display';
if (elapsed > gracePeriod + 10) {
timerDisplay.classList.add('critical');
pointsDisplay.classList.add('low');
} else if (elapsed > gracePeriod) {
timerDisplay.classList.add('warning');
pointsDisplay.classList.add('medium');
} else {
pointsDisplay.classList.add('high');
}
buzzTimer = requestAnimationFrame(updateTimer);
}
function calculatePoints(elapsed) {
if (elapsed <= gracePeriod) {
return maxPoints;
}
const overtime = elapsed - gracePeriod;
const points = Math.max(0, maxPoints - Math.floor(overtime * decayRate));
return points;
}
function buzz(playerId) {
if (currentBuzzer !== null) return;
if (!gameStarted || !startTime) return;
currentBuzzer = playerId;
buzzTime = Date.now();
const elapsed = (buzzTime - startTime) / 1000;
cancelAnimationFrame(buzzTimer);
if (window.spotifyPlayer) {
window.spotifyPlayer.pause();
}
// Markiere Spieler
document.getElementById(`player${playerId}`).classList.add('buzzed');
document.getElementById('currentPlayer').textContent = `${players[playerId - 1].name} hat gebuzzert!`;
// Deaktiviere alle Buzzer
players.forEach(player => {
document.getElementById(`buzzer${player.id}`).classList.add('disabled');
});
document.getElementById('answerSection').classList.add('active');
window.earnedPoints = calculatePoints(elapsed);
}
function setCorrectAnswer() {
if (currentGameMode === 'artist') {
correctAnswer = "{{ track.artists[0].name | clean }}";
document.getElementById('question-text').innerText = i18n.question_artist || 'Guess the artist!';
document.getElementById('answerInput').placeholder = i18n.input_artist || 'Artist name';
} else if (currentGameMode === 'title') {
correctAnswer = "{{ track.name | clean }}";
document.getElementById('question-text').innerText = i18n.question_title || 'Guess the title!';
document.getElementById('answerInput').placeholder = i18n.input_title || 'Song title';
} else if (currentGameMode === 'year') {
correctAnswer = "{{ track.album.release_date[:4] }}";
document.getElementById('question-text').innerText = i18n.question_year || 'Guess the year!';
document.getElementById('answerInput').placeholder = i18n.input_year || 'Year';
document.getElementById('answerInput').type = "number";
}
}
function searchTracks() {
const query = document.getElementById('answerInput').value;
if (query.length < 2) {
document.getElementById('searchResults').style.display = 'none';
return;
}
fetch('/search_track', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: query,
all_tracks: allTracks
})
})
.then(response => response.json())
.then(data => {
const resultsContainer = document.getElementById('searchResults');
resultsContainer.innerHTML = '';
if (data.results.length === 0) {
resultsContainer.style.display = 'none';
return;
}
data.results.forEach(result => {
const item = document.createElement('div');
item.className = 'search-item';
item.innerHTML = `<strong>${result.name}</strong> - ${result.artist}`;
item.onclick = function() {
document.getElementById('answerInput').value =
currentGameMode === 'artist' ? result.artist : result.name;
resultsContainer.style.display = 'none';
};
resultsContainer.appendChild(item);
});
resultsContainer.style.display = 'block';
})
.catch(error => {
console.error('Error searching tracks:', error);
});
}
function checkAnswer() {
const guess = document.getElementById('answerInput').value;
if (!guess) return;
fetch('/check_answer_buzzer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
guess: guess,
correct_answer: correctAnswer,
game_mode: currentGameMode,
playlist_id: '{{ playlist_id }}',
earned_points: window.earnedPoints,
player_id: currentBuzzer
})
})
.then(response => response.json())
.then(data => {
const resultContainer = document.getElementById('resultContainer');
if (data.correct) {
players[currentBuzzer - 1].score += window.earnedPoints;
updateScoreboard();
resultContainer.innerHTML = `
<div style="background:linear-gradient(135deg, rgba(76, 175, 80, 0.2) 0%, rgba(56, 142, 60, 0.2) 100%); padding:20px; border-radius:12px; border:2px solid #4CAF50;">
<h3 style="color:#4CAF50; margin-bottom:10px;">✅ ${i18n.correct || 'Correct'}</h3>
<p style="font-size:1.5em; margin:10px 0;"><strong style="color:#1DB954;">+${window.earnedPoints}</strong> Punkte für ${players[currentBuzzer - 1].name}!</p>
</div>
`;
} else {
resultContainer.innerHTML = `
<div style="background:linear-gradient(135deg, rgba(244, 67, 54, 0.2) 0%, rgba(211, 47, 47, 0.2) 100%); padding:20px; border-radius:12px; border:2px solid #f44336;">
<h3 style="color:#f44336; margin-bottom:10px;">❌ ${i18n.incorrect || 'Incorrect'}</h3>
<p style="font-size:1.1em; margin:10px 0;">${i18n.your_answer || 'Your answer'}: <strong style="color:#f44336;">${guess}</strong></p>
<p style="font-size:1.1em; margin:10px 0;">${i18n.correct_answer || 'Correct answer'}: <strong style="color:#4CAF50;">${data.correct_answer}</strong></p>
</div>
`;
}
if (data.comparison) {
resultContainer.innerHTML += `
<div style="margin-top:20px; padding:20px; background:rgba(15,20,25,0.8); border-radius:12px;">
<h4 style="color:#1DB954; margin-bottom:15px;">${i18n.song_info || 'Song Info'}:</h4>
<p style="margin:8px 0;"><strong style="color:#1DB954;">${i18n.title || 'Title'}:</strong> <span style="color:#e0e0e0;">{{ track.name | clean }}</span></p>
<p style="margin:8px 0;"><strong style="color:#1DB954;">${i18n.artist || 'Artist'}:</strong> <span style="color:#e0e0e0;">{{ track.artists[0].name | clean }}</span></p>
<p style="margin:8px 0;"><strong style="color:#1DB954;">${i18n.album || 'Album'}:</strong> <span style="color:#e0e0e0;">{{ track.album.name | clean }}</span></p>
<p style="margin:8px 0;"><strong style="color:#1DB954;">${i18n.year || 'Year'}:</strong> <span style="color:#e0e0e0;">{{ track.album.release_date[:4] }}</span></p>
<a href="{{ track.external_urls.spotify }}" target="_blank" style="display:inline-block; margin-top:10px; padding:8px 16px; background:linear-gradient(135deg, #1DB954 0%, #1ed760 100%); color:#fff; text-decoration:none; border-radius:20px; font-weight:600; transition:all 0.3s ease;">${i18n.open_on_spotify || 'Open on Spotify'}</a>
</div>
`;
}
document.getElementById('nextQuestionBtn').style.display = 'inline-block';
})
.catch(error => {
console.error('Error checking answer:', error);
});
}
function switchGameMode(mode) {
window.location.href = `/reset_quiz/{{ playlist_id }}?next_mode=${mode}&buzzer=1&local_multiplayer=1`;
}
function getOption(key, defaultValue) {
return localStorage.getItem(key) || defaultValue;
}
window.onload = function() {
console.log('Page loaded, initializing...');
document.getElementById('startPosition').value = getOption('startPosition', 'start');
maxPoints = parseInt(localStorage.getItem('buzzer_max_points')) || 1000;
gracePeriod = parseInt(localStorage.getItem('buzzer_grace_period')) || 5;
decayRate = parseInt(localStorage.getItem('buzzer_decay_rate')) || 50;
document.getElementById('pointsDisplay').textContent = maxPoints + ' Punkte';
// Initialisiere Spieler
initializePlayers();
// Dynamisches Scoreboard erstellen
createScoreboard();
// Dynamische Buzzer-Buttons erstellen
createBuzzerButtons();
console.log('Initialization complete');
};
function createScoreboard() {
console.log('Creating scoreboard for', players.length, 'players');
const scoreboard = document.getElementById('scoreboard');
if (!scoreboard) {
console.error('Scoreboard element not found!');
return;
}
scoreboard.innerHTML = '';
players.forEach(player => {
const card = document.createElement('div');
card.className = 'player-card';
card.id = `player${player.id}`;
card.innerHTML = `
<div class="player-name">${player.name}</div>
<div class="player-score">${player.score}</div>
`;
scoreboard.appendChild(card);
});
console.log('Scoreboard created');
}
function createBuzzerButtons() {
console.log('Creating buzzer buttons for', players.length, 'players');
const grid = document.getElementById('buzzerGrid');
if (!grid) {
console.error('Buzzer grid element not found!');
return;
}
grid.innerHTML = `
<button class="player-buzzer start" id="startButton" onclick="startGame()">
▶️<br>START
</button>
`;
players.forEach(player => {
const button = document.createElement('button');
button.className = 'player-buzzer disabled';
button.id = `buzzer${player.id}`;
button.onclick = () => buzz(player.id);
button.innerHTML = `🔴<br>${player.name}`;
grid.appendChild(button);
});
console.log('Buzzer buttons created');
}
</script>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div class="quiz-container">
<!-- Scoreboard -->
<div class="scoreboard" id="scoreboard">
<!-- Dynamisch generiert via JavaScript -->
</div>
<input type="hidden" id="device_id" value="">
<h2 id="question-text" style="text-align:center; margin:20px 0;">{{ translations['question_title'] }}</h2>
<!-- Timer & Points Display -->
<div class="buzzer-timer" id="buzzerTimer">0.00s</div>
<div class="points-display high" id="pointsDisplay">1000 Punkte</div>
<div class="current-player" id="currentPlayer">Drücke START um zu beginnen</div>
<!-- Buzzer Buttons -->
<div class="buzzer-grid" id="buzzerGrid">
<!-- Dynamisch generiert via JavaScript -->
</div>
<!-- Answer Section -->
<div class="answer-section" id="answerSection">
<h3 style="margin-bottom:15px;">{{ translations['enter_answer'] if translations.get('enter_answer') else 'Enter your answer' }}:</h3>
<input type="text" id="answerInput" placeholder="{{ translations['input_title'] }}" oninput="searchTracks()">
<button class="btn" onclick="checkAnswer()">{{ translations['answer_button'] if translations.get('answer_button') else 'Submit' }}</button>
<div id="searchResults" class="search-results"></div>
<div id="resultContainer" class="result-container"></div>
<a id="nextQuestionBtn" href="/quiz/{{ playlist_id }}?mode={{ game_mode }}&buzzer=1&local_multiplayer=1" class="btn" style="display: none; margin-top:15px;">{{ translations['next_question'] if translations.get('next_question') else 'Next Question' }}</a>
</div>
<!-- Game Mode Switcher -->
<div style="text-align:center; margin-top:30px;">
<button class="btn {{ 'btn-success' if game_mode == 'artist' else 'btn-secondary' }}" onclick="switchGameMode('artist')">{{ translations['guess_artist'] }}</button>
<button class="btn {{ 'btn-success' if game_mode == 'title' else 'btn-secondary' }}" onclick="switchGameMode('title')">{{ translations['guess_title'] }}</button>
<button class="btn {{ 'btn-success' if game_mode == 'year' else 'btn-secondary' }}" onclick="switchGameMode('year')">{{ translations['guess_year'] }}</button>
</div>
<!-- Navigation -->
<div style="text-align:center; margin-top:30px;">
<a href="/reset_quiz/{{ playlist_id }}?buzzer=1&local_multiplayer=1" class="btn btn-danger">{{ translations['end_quiz'] if translations.get('end_quiz') else '🏁 End Quiz' }}</a>
<a href="/playlists" class="btn btn-secondary">{{ translations['back_to_playlists'] if translations.get('back_to_playlists') else '⬅️ Back' }}</a>
</div>
</div>
</body>
</html>