Resolve "Richtig schönes, cooles Siegerpodest." #273

Merged
ben8 merged 2 commits from 141-richtig-schones-cooles-siegerpodest-2 into master 2025-07-29 17:02:52 +00:00
12 changed files with 1257 additions and 126 deletions

View File

@@ -128,6 +128,15 @@ class GameConsumer(AsyncWebsocketConsumer):
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.advance_to_next_question()
elif message_type == 'winner_podest':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.show_winner_podest()
elif message_type == 'scoreboard':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.show_scoreboard()
elif message_type == 'finish_game':
host_id = text_data_json['host_id']
@@ -430,6 +439,26 @@ class GameConsumer(AsyncWebsocketConsumer):
'redirect_url': reverse('play:question', kwargs={'join_code': self.join_code})
}
)
elif result == 'winner_podest':
# Notify clients to redirect to next question
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'show_winner_podest',
'redirect_url': reverse('play:winner_podest', kwargs={'join_code': self.join_code})
}
)
elif result == 'scoreboard':
# Notify clients to redirect to next question
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'show_scoreboard',
'redirect_url': reverse('play:scoreboard', kwargs={'join_code': self.join_code})
}
)
@database_sync_to_async
def _show_scores(self):
@@ -476,3 +505,48 @@ class GameConsumer(AsyncWebsocketConsumer):
'redirect_url': reverse('play:finished', kwargs={'join_code': self.join_code})
}
)
@database_sync_to_async
def _show_winner_podest(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz_game.current_state = 'winner_podest'
quiz_game.save()
return True
except QuizGame.DoesNotExist:
return False
async def show_winner_podest(self):
success = await self._show_winner_podest()
if success:
# Notify clients to redirect to finished page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'show_winner_podest',
'redirect_url': reverse('play:winner_podest', kwargs={'join_code': self.join_code})
}
)
@database_sync_to_async
def _show_scoreboard(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz_game.current_state = 'scoreboard'
quiz_game.save()
return True
except QuizGame.DoesNotExist:
return False
async def show_scoreboard(self):
success = await self._show_scoreboard()
if success:
# Notify clients to redirect to finished page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'show_scoreboard',
'redirect_url': reverse('play:scoreboard', kwargs={'join_code': self.join_code})
}
)

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-07-25 16:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0013_quizanswer_answer_text_quizanswer_answers_order_and_more'),
]
operations = [
migrations.AlterField(
model_name='quizgame',
name='current_state',
field=models.CharField(choices=[('lobby', 'In Lobby'), ('question', 'Frage läuft'), ('scores', 'Punkteübersicht'), ('winner_podest', 'Siegerpodest'), ('finished', 'Beendet')], default='lobby', max_length=20),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-07-28 13:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0014_alter_quizgame_current_state'),
]
operations = [
migrations.AlterField(
model_name='quizgame',
name='current_state',
field=models.CharField(choices=[('lobby', 'In Lobby'), ('question', 'Frage läuft'), ('scores', 'Punkteübersicht'), ('winner_podest', 'Siegerpodest'), ('finished', 'Beendet'), ('scoreboard', 'Scoreboard')], default='lobby', max_length=20),
),
]

View File

@@ -10,7 +10,10 @@ class QuizGame(models.Model):
('lobby', 'In Lobby'),
('question', 'Frage läuft'),
('scores', 'Punkteübersicht'),
('finished', 'Beendet')
('winner_podest','Siegerpodest'),
('finished', 'Beendet'),
('scoreboard', 'Scoreboard'),
]
host_id = models.CharField(max_length=200, unique=True)

View File

@@ -12,6 +12,8 @@ urlpatterns = [
path('game/<str:join_code>/waiting', views.waiting_room, name='waiting'),
path('game/<str:join_code>/scores', views.scores, name='scores'),
path('game/<str:join_code>/finished', views.finished, name='finished'),
path('game/<str:join_code>/winner_podest', views.winner_podest, name='winner_podest'),
path('game/<str:join_code>/scoreboard', views.scoreboard, name='scoreboard'),
path('select_mode/<str:join_code>', views.select_mode, name='select_mode'),
path('selected_mode/<str:join_code>', views.selected_mode, name='selected_mode'),
]

View File

@@ -188,6 +188,49 @@ def scores(request, join_code):
'question_index': quiz_game.current_question_index,
'total_questions': len(questions)
})
def scoreboard(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
# Verify game state
if quiz_game.current_state != 'scoreboard':
return redirect('play:lobby', join_code=join_code)
# Get participants sorted by score
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game).order_by('-score')
# Check if user is host
is_host = False
if 'host_id' in request.COOKIES and request.COOKIES['host_id'] == quiz_game.host_id:
is_host = True
# Get current question for context
questions = quiz_game.quiz_id.questions.all()
current_question = questions[quiz_game.current_question_index]
question_data = json.loads(current_question.data)
try:
my_participant = request.session.get('my_participant-participant_id')
participant = QuizGameParticipant.objects.get(participant_id=my_participant)
my_participant=participant
except:
participant = QuizGameParticipant.objects.none()
return render(request, 'play/game/scoreboard.html', {
'quiz_game': quiz_game,
'participants': participants,
'participant': participant,
'my_participant':my_participant,
'is_host': is_host,
'host_id': quiz_game.host_id if is_host else None,
'is_last_question': quiz_game.current_question_index + 1 >= len(questions),
'question_data': question_data,
'question_index': quiz_game.current_question_index,
'total_questions': len(questions)
})
"""
def finished(request, join_code):
@@ -225,6 +268,45 @@ def finished(request, join_code):
"""
def winner_podest(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
# Verify game state
if quiz_game.current_state != 'winner_podest':
return redirect('play:lobby', join_code=join_code)
# Get participants sorted by score
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game).order_by('-score')
# Check if user is host
is_host = False
if 'host_id' in request.COOKIES and request.COOKIES['host_id'] == quiz_game.host_id:
is_host = True
# Get current question for context
questions = quiz_game.quiz_id.questions.all()
current_question = questions[quiz_game.current_question_index]
question_data = json.loads(current_question.data)
try:
my_participant = request.session.get('my_participant-participant_id')
participant = QuizGameParticipant.objects.get(participant_id=my_participant)
my_participant=participant
except:
participant = QuizGameParticipant.objects.none()
return render(request, 'play/game/winner_podest.html', {
'quiz_game': quiz_game,
'participants': participants,
'participant': participant,
'my_participant':my_participant,
'is_host': is_host,
'host_id': quiz_game.host_id if is_host else None,
'is_last_question': quiz_game.current_question_index + 1 >= len(questions),
'question_data': question_data,
'question_index': quiz_game.current_question_index,
'total_questions': len(questions)
})
def finished(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
@@ -262,7 +344,6 @@ def finished(request, join_code):
return redirect('home')
def question(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code)

View File

@@ -1,5 +1,5 @@
<div class="grid place-content-center gap-2 w-full mt-4">
<div class="flex space-x-2 pb-4">
<div class="bottom-0 left-0 w-full p-2 flex justify-center space-x-4 z-50">
<p class="text-sm font-extralight dark:text-white"><a href="/impress">Impressum</a></p>
<p class="text-sm font-extralight dark:text-white"><a href="/privacy">Datenschutz</a></p>
<p class="text-sm font-extralight dark:text-white"><a href="https://edugit.org/enrichment-2024/qivip">Repository</a></p>

View File

@@ -153,7 +153,7 @@
{% if request.session.mode == "learn" %}
<form action="">
<input id="textanswer" type="text" placeholder="DEINE ANTWORT"
class="answer-option text-center p-2 bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 answer-option lg:w-80 dark:bg-transparent dark:text-white dark:hover:bg-[#2a2f3a] ">
class="answer-option text-center mb-4 p-2 bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 answer-option lg:w-80 dark:bg-transparent dark:text-white dark:hover:bg-[#2a2f3a] ">
<button
onclick="submitAnswer( -1 );"

View File

@@ -160,7 +160,7 @@
{% elif question_data.type == "input" %}
<form action="">
<input id="textanswer" type="text" placeholder="DEINE ANTWORT"
class="answer-option text-center p-2 bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 answer-option lg:w-80 dark:bg-transparent dark:text-white dark:hover:bg-[#2a2f3a]">
class="answer-option text-center p-2 mb-4 bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 answer-option lg:w-80 dark:bg-transparent dark:text-white dark:hover:bg-[#2a2f3a]">
<button
onclick="submitAnswer( -1 );"
class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-lg transition-colors duration-200 answer-option "
@@ -318,6 +318,7 @@ function updateOrderNumbers() {
function submitOrderAnswer() {
if (hasAnswered) return;
const order_button=document.getElementById("send_order")
order_button.disabled=true;
order_button.classList.add('opacity-50');

View File

@@ -2,9 +2,9 @@
{% block content %}
<div class="container mx-auto px-4">
<div class="bg-white rounded-lg shadow-md p-6 mb-6 dark:bg-transparent dark:text-white">
<div class="bg-white rounded-lg shadow-md p-6 mb-6 dark:bg-transparent dark:text-white ">
<h1 class="text-3xl font-bold mb-6">Ergebnisse</h1>
{% if request.session.mode == "learn" or is_host %}
<div class="mb-6">
<h2 class="text-xl font-bold mb-4">Aktuelle Frage</h2>
@@ -65,14 +65,57 @@
</div>
</div>
{% endif %}
{% else %}
<div class="flex flex-col items-center justify-center text-center pt-4">
{% if participant.last_answer_correct %}
<!-- Richtig-Symbol -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"
class="text-green-500 size-40">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<!-- Feedback-Text -->
<div id="antwort-richtig"
class="pt-3 text-gray-600 dark:text-white font-bold max-w-xs md:max-w-sm">
Hier erscheint gleich dein Feedback...
</div>
{% elif participant.last_answer_correct == False %}
<!-- Falsch-Symbol -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"
class="text-red-500 size-40">
<path stroke-linecap="round" stroke-linejoin="round"
d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<!-- Feedback-Text -->
<div id="antwort-falsch"
class="pt-3 text-gray-600 dark:text-white font-bold max-w-xs md:max-w-sm">
Hier erscheint gleich dein Feedback...
</div>
{% endif %}
<p class="text-blue-600 font-bold text-md">+{{ participant.last_score }} Punkte</p>
</div>
{% endif %}
</div>
<div class="mb-6">
{% if request.session.mode == "learn" %}
<h2 class="text-xl font-bold mb-4">Punktestand</h2>
{% endif %}
<div id="scoreboard" class="leading-relaxed">
{% for participant in participants %}
{% if request.session.mode == "learn" %}
{% if participant == my_participant or is_host %}
<div class="dark:bg-transparent mt-2 shadow-sm p-4 bg-gray-100 rounded-lg border-2 border {% if forloop.counter <= 3 %}border-yellow-500{% else %}border-transparent{% endif %}">
<div class="flex flex-col items-start gap-1">
@@ -92,60 +135,14 @@
<p data-score="{{ participant.score }}" data-last-score="{{ participant.last_score }}" class="dark:text-white text-gray-600 font-bold show-score" id="score-{{ forloop.counter0 }}">{{ participant.score }} Punkte</p>
{% if participant == my_participant or request.session.mode == "learn"%}
<p class="text-blue-600 font-bold text-sm">+{{ participant.last_score }} Punkte</p>
{% endif %}
{% endif %}
</div>
<script>
async function start() {
elements=document.getElementsByClassName("show-score");
for (let i = 0; i < elements.length; i++) {
const el = elements[i];
let counter=Number(el.getAttribute('data-score'))-Number(el.getAttribute('data-last-score'));
let goal=Number(el.getAttribute('data-score'));
let interval= setInterval(function(){
el.innerHTML = counter + " Punkte";
if(counter<=goal){
el.innerHTML = counter + " Punkte";
counter+=8;
}
else{
el.innerHTML = goal + " Punkte";
clearInterval(interval);
}
}, 20);}}
function bounce(){
const names = document.getElementsByClassName("display_name");
for (let i = 0; i < names.length; i++) {
names[i].classList.add("animate-bounce");
}
}
{% if is_last_question %}
bounce();
{% else %}
start();
{% endif %}
</script>
@@ -189,13 +186,29 @@
{% if is_host %}
{% if is_last_question %}
{% if is_last_question and request.session.mode != "learn" %}
<button id="winner-button" class="mt-4 w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
Siegerpodest
</button>
{% else %}
<button id="finish-button" class="mt-4 w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
Quiz beenden
</button>
{% endif %}
{% else %}
<button id="next-button" class="mt-4 w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
{% if request.session.mode == "learn" %}
<button id="next-button" class="mt-4 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>
{% else %}
<button id="scoreboard-button" class="mt-4 w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
Punkteübersicht
</button>
{% endif %}
{% endif %}
{% endif %}
</div>
@@ -205,68 +218,6 @@
{% block extra_js %}
{% if is_last_question %}
<script>
// Hilfsfunktion für Pausen
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
document.addEventListener('DOMContentLoaded', async function() {
const items = document.querySelectorAll('#scoreboard div');
const top3 = Array.from(items).slice(0, 3); // Nur die ersten 3 animieren
// Sofort Startzustände für Top3 setzen, die anderen sofort sichtbar
items.forEach((el, index) => {
if (index >= 3) {
el.style.opacity = '1';
el.style.transform = 'translateY(0)';
} else {
el.style.opacity = '0';
el.style.transform = 'translateY(20px)';
}
});
try{
document.getElementById("finish-button").style.display = 'none';}
catch{}
// Warten bevor Animation startet
await sleep(500); // <<< hier die Pause
// Transition-Eigenschaft setzen nach Render-Zyklus
requestAnimationFrame(() => {
top3.forEach(el => {
el.style.transition = 'all 0.8s ease';
});
// Animation für Top 3 starten von Platz 3 zu 1
top3.reverse().forEach((el, index) => {
setTimeout(() => {
el.style.opacity = '1';
el.style.transform = 'translateY(0)';
if (index === top3.length - 1) {
triggerConfetti();
try{
document.getElementById("finish-button").style.display = 'block';}
catch{}
}
}, index * 1000); // Zeitversetzt aufdecken
});
});
});
function triggerConfetti() {
const end = Date.now() + 15 * 1000;
(function frame() {
confetti({ particleCount: 2, angle: 60, spread: 55, origin: { x: 0 } });
confetti({ particleCount: 2, angle: 120, spread: 55, origin: { x: 1 } });
if (Date.now() < end) requestAnimationFrame(frame);
})();
}
</script>
{% endif %}
{% include 'play/game/common_scripts.html' %}
{% if is_host %}
@@ -278,6 +229,61 @@
<script>
function triggerConfetti() {
const end = Date.now() + 30 * 1000;
(function frame() {
confetti({ particleCount: 2, angle: 60, spread: 55, origin: { x: 0 } });
confetti({ particleCount: 2, angle: 120, spread: 55, origin: { x: 1 } });
if (Date.now() < end) requestAnimationFrame(frame);
})();
}
async function start() {
elements=document.getElementsByClassName("show-score");
for (let i = 0; i < elements.length; i++) {
const el = elements[i];
let counter=Number(el.getAttribute('data-score'))-Number(el.getAttribute('data-last-score'));
let goal=Number(el.getAttribute('data-score'));
let interval= setInterval(function(){
el.innerHTML = counter + " Punkte";
if(counter<=goal){
el.innerHTML = counter + " Punkte";
counter+=8;
}
else{
el.innerHTML = goal + " Punkte";
clearInterval(interval);
}
}, 20);}}
function bounce(){
const names = document.getElementsByClassName("display_name");
for (let i = 0; i < names.length; i++) {
names[i].classList.add("animate-bounce");
}
}
{% if is_last_question %}
start();
bounce();
{% if request.session.mode == "learn" %}
triggerConfetti();
{% endif %}
{% else %}
start();
{% endif %}
const richtigeAntworten = [
"Deine Antwort ist nicht falsch. Sie ist richtig!",
"Ja, du kannst es!",
@@ -351,10 +357,11 @@ if (antwortRichtig) {
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') {
data.redirect_url &&
(data.action === 'next_question' || data.action === 'finish_game' || data.action === 'show_winner_podest'|| data.action === 'show_scoreboard')) {
window.location.href = data.redirect_url;
}
else if (data.type === 'answer_stats') {
updateAnswerStats(data.stats);
}
};
@@ -380,7 +387,30 @@ if (antwortRichtig) {
}));
});
}
var podestButton = document.getElementById('winner-button');
if (podestButton) {
podestButton.addEventListener('click', function() {
gameSocket.send(JSON.stringify({
'type': 'winner_podest',
'host_id': hostId
}));
});
}
{% else %}
var scoreboardButton = document.getElementById('scoreboard-button');
if (scoreboardButton) {
scoreboardButton.addEventListener('click', function() {
gameSocket.send(JSON.stringify({
'type': 'scoreboard',
'host_id': hostId
}));
});
}
var nextButton = document.getElementById('next-button');
if (nextButton) {
nextButton.addEventListener('click', function() {

View File

@@ -0,0 +1,310 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mx-auto px-4">
<div class="bg-white rounded-lg shadow-md p-6 mb-6 dark:bg-transparent dark:text-white">
<h1 class="text-3xl font-bold mb-6">Ergebnisse</h1>
<div class="mb-6">
{% if request.session.mode != "learn" %}
<h2 class="text-xl font-bold mb-4">Punktestand</h2>
{% endif %}
<div id="scoreboard" class="leading-relaxed">
{% for participant in participants %}
{% if request.session.mode != "learn" %}
{% if participant == my_participant or is_host %}
<div class="dark:bg-transparent mt-2 shadow-sm p-4 bg-gray-100 rounded-lg border-2 border
{% if forloop.counter <= 3 %}
{% if forloop.counter == 1 %}border-yellow-400{% endif %}
{% if forloop.counter == 2 %}border-gray-400{% endif %}
{% if forloop.counter == 3 %}border-orange-400{% endif %}
{% else %}
border-transparent{% endif %}">
<div class="flex flex-col items-start gap-1">
<div class="text-4xl font-bold flex items-center gap-2">
{% if forloop.counter == 1 %}
🥇
{% elif forloop.counter == 2 %}
🥈
{% elif forloop.counter == 3 %}
🥉
{% else %}
{{ forloop.counter }}.
{% endif %}
<span class="display_name text-gray-600 font-black text-3xl dark:text-white " id="display_name-{{ forloop.counter0 }}">{{ participant.display_name }}</span>
</div class="flex flex-col items-start">
<p data-score="{{ participant.score }}" data-last-score="{{ participant.last_score }}" class="dark:text-white text-gray-600 font-bold show-score text-xl" id="score-{{ forloop.counter0 }}">{{ participant.score }} Punkte</p>
{% if participant == my_participant or request.session.mode == "learn"%}
<p class="text-blue-600 font-bold text-md">+{{ participant.last_score }} Punkte</p>
{% endif %}
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% if is_host %}
{% if is_last_question %}
{% if is_last_question and request.session.mode != "learn" %}
<button id="winner-button" class="mt-4 w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
Siegerpodest
</button>
{% else %}
<button id="finish-button" class="mt-4 w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
Quiz beenden
</button>
{% endif %}
{% else %}
<button id="next-button" class="mt-4 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 %}
{% if is_last_question %}
{% endif %}
{% include 'play/game/common_scripts.html' %}
{% if is_host %}
{% include 'play/game/sound.html' %}
{% endif %}
<script>
async function start() {
elements=document.getElementsByClassName("show-score");
for (let i = 0; i < elements.length; i++) {
const el = elements[i];
let counter=Number(el.getAttribute('data-score'))-Number(el.getAttribute('data-last-score'));
let goal=Number(el.getAttribute('data-score'));
let interval= setInterval(function(){
el.innerHTML = counter + " Punkte";
if(counter<=goal){
el.innerHTML = counter + " Punkte";
counter+=8;
}
else{
el.innerHTML = goal + " Punkte";
clearInterval(interval);
}
}, 20);}}
function bounce(){
const names = document.getElementsByClassName("display_name");
for (let i = 0; i < names.length; i++) {
names[i].classList.add("animate-bounce");
}
}
{% if is_last_question %}
start();
bounce();
{% else %}
start();
{% endif %}
const richtigeAntworten = [
"Deine Antwort ist nicht falsch. Sie ist richtig!",
"Ja, du kannst es!",
"So kann es weiter gehen.",
"Ja, deine Antwort ist korrekt. Was soll ich dazu noch sagen? SUPER!",
"SUPER GUT.",
"Volltreffer!",
"Bravo! Das sitzt!",
"Na bitte, geht doch!",
"Schön. Einfach schön.",
"Exzellent kombiniert, Sherlock!",
"Punkt für dich!",
"Das war meisterhaft.",
"Korrekt - du scheinst das zu können!",
"So sieht Erfolg aus!",
"Glänzend gelöst!",
"Das war keine Glückssache - das war Können!",
"Treffer, versenkt!",
"Du hast es drauf!",
"Da merkt man: Profi am Werk.",
"Richtige Antwort - dafür gibt es viele Punkte!",
"Weiter so!",
"Wow - das war richtig gut!"
];
const falscheAntworten = [
"Oops! War wohl nicht dein Tag.",
"Knapp daneben ist auch vorbei!",
"Das war... kreativ!",
"Wenn man schon alles wüsste, wäre das Quiz doch sinnlos.",
"Deine Antwort war einzigartig - leider falsch!",
"Na gut, wenigstens hast du es probiert.",
"Das war eine interessante Theorie...",
"Ein Genie irrt sich auch mal.",
"Eine gute Antwort - aber nicht auf diese Frage.",
"Das ist leider falsch. Mehr gibt es nicht zu sagen.",
"Gute Idee! Aber leider nicht richtig.",
"Oh weh! Ein Fehler. Naja, einer ist keiner.",
"Die Antwort klang richtig, sie ist es aber nicht.",
"Interessanter Ansatz. Nicht hilfreich, aber interessant.",
"Nenn das lieber „alternative Fakten“.",
"Du hast die richtige Antwort doch gewusst: 'Die Seite hat nur nicht geladen.'",
"Jeder fängt mal klein an!",
"Kopf hoch - weiter geht's!",
"Nobody is perfect!",
"Du schaffst das - vielleicht ab jetzt.",
"Falsche Antwort, aber guter Wille!",
"Übung macht den Quiz-Meister.",
"Beim nächsten Mal triffst du!",
"Das war nur zum Aufwärmen, oder?",
"Kein Problem - weiter geht's.",
"Gute Frage - aber falsche Antwort."
];
const antwortRichtig = document.getElementById("antwort-richtig");
const antwortFalsch = document.getElementById("antwort-falsch");
if (antwortRichtig) {
antwortRichtig.textContent = richtigeAntworten[Math.floor(Math.random() * richtigeAntworten.length)];
} else if (antwortFalsch) {
antwortFalsch.textContent = falscheAntworten[Math.floor(Math.random() * falscheAntworten.length)];
}
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' || data.action === 'show_winner_podest'|| data.action === 'show_scoreboard')) {
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
}));
});
}
var podestButton = document.getElementById('winner-button');
if (podestButton) {
podestButton.addEventListener('click', function() {
gameSocket.send(JSON.stringify({
'type': 'winner_podest',
'host_id': hostId
}));
});
}
{% else %}
var scoreboardButton = document.getElementById('scoreboard-button');
if (scoreboardButton) {
scoreboardButton.addEventListener('click', function() {
gameSocket.send(JSON.stringify({
'type': 'scoreboard',
'host_id': hostId
}));
});
}
var nextButton = document.getElementById('next-button');
if (nextButton) {
nextButton.addEventListener('click', function() {
gameSocket.send(JSON.stringify({
'type': 'next_question',
'host_id': hostId
}));
}, { once: true });
}
{% endif %}
{% endif %}
function updateAnswerStats(stats) {
const total = Object.values(stats).reduce((a, b) => a + b, 0) || 1;
for (var optionIndex in stats) {
if (stats.hasOwnProperty(optionIndex)) {
var countElement = document.getElementById('option-count-' + optionIndex);
var fillElement = document.getElementById('option-fill-' + optionIndex);
if (countElement) {
countElement.textContent = stats[optionIndex] + ' Antwort(en)';
}
if (fillElement) {
const percentage = Math.round((stats[optionIndex] / total) * 100);
fillElement.style.width = percentage + '%';
}
}
}
}
// Request answer stats when page loads
gameSocket.onopen = function(e) {
gameSocket.send(JSON.stringify({
'type': 'get_answer_stats'
}));
};
</script>
{% endblock %}

View File

@@ -0,0 +1,594 @@
{% extends 'base.html' %}
{% block content %}
{% if is_last_question and is_host %}
<!-- Siegerpodest -->
<div class="min-h-screen">
<h2 class="w-full flex justify-center dark:text-white font-bold text-2xl ">&#127942; Siegerpodest</h2>
<h4 class="w-full flex justify-center dark:text-white font-bold">Wer ist hier der Quiz-Master? </h4>
{% with participant=participants.0 %}
<div class="w-full flex justify-center">
<h1 id="winner-text" class="bg-gradient-to-r from-yellow-400 via-yellow-300 to-yellow-600 bg-clip-text text-transparent font-bold hidden animate-ping px-2 max-w-screen-sm text-center break-words">
Glückwunsch, {{ participant.display_name }}!
</h1>
</div>
{% endwith %}
<div id="podest-container" class="w-full absolute bottom-0 flex justify-center items-end gap-2 md:gap-4 mb-2 dark:text-white">
<!-- Platz 2 links -->
{% with participant=participants.1 %}
<div class="flex flex-col shrink items-center break-words max-w-[6rem] md:max-w-[8rem] lg:max-w-[10rem] overflow-x-auto pt-10">
<span class="display_name p-2 text-gray-600 font-black text-xl md:text-2xl lg:text-3xl opacity-0 transition-opacity duration-1000 dark:text-white hyphens-auto" id="display_name-1">{{ participant.display_name }}</span>
<p data-score="{{ participant.score }}" data-last-score="{{ participant.last_score }}" class="opacity-0 transition-opacity duration-1000 dark:text-white font-bold show-score text-sm md:text-lg lg:text-xl text-gray-400" id="score-1">{{ participant.score }}</p>
<div data-final-height="33" id="podium-1" class="border-2 border-gray-200 transition-all duration-1000 ease-out overflow-hidden podium h-24 md:h-[12rem] lg:h-[16rem] w-24 md:w-32 lg:w-40 rounded-t-md font-bold text-center text-base shadow-lg bg-gradient-to-b from-gray-400 via-gray-300 to-gray-300 flex flex-col items-center justify-center text-black dark:text-white cursor-pointer leading-tight">
<span class="size-10 md:size-18 rounded-full bg-white text-black text-center flex items-center justify-center font-bold text-3xl md:text-5xl lg:text-6xl">🥈</span>
</div>
</div>
{% endwith %}
<!-- Platz 1 mitte -->
{% with participant=participants.0 %}
<div class="flex flex-col shrink items-center break-words max-w-[6rem] md:max-w-[8rem] lg:max-w-[10rem] overflow-x-auto pt-10">
<div id="resultFirst"></div>
<span class="display_name p-2 text-gray-600 font-black text-xl md:text-3xl lg:text-4xl opacity-0 transition-opacity duration-1000 bg-gradient-to-r from-yellow-400 via-yellow-300 to-yellow-600 bg-clip-text text-transparent hyphens-auto" id="display_name-0">{{ participant.display_name }}</span>
<p data-score="{{ participant.score }}" data-last-score="{{ participant.last_score }}" class="opacity-0 transition-opacity duration-1000 dark:text-white text-yellow-500 font-bold show-score text-sm md:text-lg lg:text-xl" id="score-0">{{ participant.score }}</p>
<div data-final-height="45" id="podium-0" class="border-2 border-yellow-500 transition-all duration-1000 ease-out overflow-hidden podium h-32 md:h-[16rem] lg:h-[22rem] w-24 md:w-32 lg:w-40 rounded-t-md font-bold text-center text-base shadow-lg bg-gradient-to-b from-yellow-400 via-yellow-300 to-yellow-600 flex flex-col items-center justify-center text-black dark:text-white cursor-pointer leading-tight">
<span id="firstMedal" class="size-12 md:size-22 rounded-full bg-white text-black text-center flex items-center justify-center font-bold text-4xl md:text-6xl lg:text-7xl">🥇</span><br>
</div>
</div>
{% endwith %}
<!-- Platz 3 rechts -->
{% with participant=participants.2 %}
<div class="flex flex-col shrink items-center break-words max-w-[6rem] md:max-w-[8rem] lg:max-w-[10rem] overflow-x-auto pt-10">
<span class="display_name p-2 text-gray-600 font-black text-xl md:text-2xl lg:text-3xl dark:text-white opacity-0 transition-opacity duration-1000 hyphens-auto" id="display_name-2">{{ participant.display_name }}</span>
<p data-score="{{ participant.score }}" data-last-score="{{ participant.last_score }}" class="opacity-0 transition-opacity duration-1000 dark:text-white text-orange-500 font-bold show-score text-sm md:text-lg lg:text-xl" id="score-2">{{ participant.score }}</p>
<div data-final-height="25" id="podium-2" class="border-2 border-orange-500 transition-all duration-1000 ease-out overflow-hidden podium h-20 md:h-[10rem] lg:h-[14rem] w-24 md:w-32 lg:w-40 rounded-t-md font-bold text-center text-base shadow-lg bg-gradient-to-b from-orange-500 via-orange-400 to-orange-700 flex flex-col items-center justify-center text-black dark:text-white cursor-pointer leading-tight">
<span class="size-10 md:size-18 rounded-full bg-white text-black text-center flex items-center justify-center font-bold text-3xl md:text-5xl lg:text-6xl">🥉</span>
</div>
</div>
{% endwith %}
</div>
</div>
<div id="thanks" class="hidden -mt-10">
<div class="w-full flex justify-center">
<h1 class="bg-gradient-to-r from-yellow-400 via-yellow-300 to-yellow-600 bg-clip-text text-transparent font-bold px-2 max-w-screen-sm text-center break-words">
Vielen Dank fürs Spielen!
</h1>
</div>
<div class="flex justify-center">
<button id="finish-button" class=" w-3/4 md:w-1/2 bottom-0 left-0 p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
Quiz beenden
</button>
</div>
</div>
{% else %}
<div class="container mx-auto px-4">
{% with participant=participants.0 %}
<div class="w-full flex justify-center">
<h1 id="winner-text" class="bg-gradient-to-r from-yellow-400 via-yellow-300 to-yellow-600 bg-clip-text text-transparent font-bold hidden animate-ping px-2 max-w-screen-sm text-center break-words">
Glückwunsch, {{ participant.display_name }}!
</h1>
</div>
{% endwith %}
<div class="bg-white rounded-lg shadow-md p-6 mb-6 dark:bg-transparent dark:text-white">
<h1 id="result-text" class="text-3xl w-full flex justify-center dark:text-white font-bold">&#127942; Wer ist hier der Quiz-Master?</h1>
<h4 id="info" class="mt-4 w-full flex justify-center dark:text-white font-bold">Nach der Siegerehrung siehst du deine Platzierung!</h4>
<div id="participant-score" class="mb-6 ">
<div id="scoreboard" class="leading-relaxed">
{% for participant in participants %}
{% if request.session.mode != "learn" %}
{% if participant == my_participant or is_host %}
<div id="player_score" class="hidden dark:bg-transparent mt-2 shadow-sm p-4 bg-gray-100 rounded-lg border-2 border {% if forloop.counter <= 3 %}
{% if forloop.counter == 1 %}border-yellow-400{% endif %}
{% if forloop.counter == 2 %}border-gray-400{% endif %}
{% if forloop.counter == 3 %}border-orange-400{% endif %}
{% else %}
border-transparent{% endif %}">
<div class="flex flex-col items-start gap-1">
<div class="text-4xl font-bold flex items-center gap-2">
{% if forloop.counter == 1 %}
🥇
{% elif forloop.counter == 2 %}
🥈
{% elif forloop.counter == 3 %}
🥉
{% else %}
{{ forloop.counter }}.
{% endif %}
<span class="display_name text-gray-600 font-black text-3xl dark:text-white " id="display_name-{{ forloop.counter0 }}">{{ participant.display_name }}</span>
</div class="flex flex-col items-start">
<p data-score="{{ participant.score }}" data-last-score="{{ participant.last_score }}" class="dark:text-white text-gray-600 font-bold show-score text-xl" id="score-{{ forloop.counter0 }}">{{ participant.score }} Punkte</p>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% endif %}
<style>
.podium {
height: 0;
}
.spotlight-wrapper::before {
content: "";
position: absolute;
top: -800px; /* Start oben außerhalb */
left: 50%;
transform: translateX(-50%);
width: 15rem;
height: 700px;
pointer-events: none;
background: radial-gradient(
ellipse 60% 100% at center bottom,
rgba(254, 254, 178, 0.9) 0%,
rgba(252, 236, 132, 0.7) 25%,
rgba(255, 230, 130, 0.5) 55%,
rgba(255, 210, 100, 0.1) 85%,
rgba(255, 180, 70, 0) 100%
);
filter: blur(24px) brightness(1.2);
opacity: 0; /* Start unsichtbar */
animation: slideInOutSpotlight 6s ease forwards , fadeOutSpotlight 6s 6s ease forwards; /* Dauer anpassen */
z-index: -1;
box-shadow: 0 0 60px 30px rgba(255, 240, 150, 0.15);
}
@keyframes slideInOutSpotlight {
0% {
top: -800px;
opacity: 0;
}
20% {
top: -640px; /* Endposition */
opacity: 1;
}
100% {
top: -640px;
opacity: 1;
}
}
@keyframes fadeOutSpotlight {
0% {
top: -640px;
opacity: 1;
}
100% {
top: -640px;
opacity: 0;
}
}
</style>
{% endblock %}
</div>
{% block extra_js %}
{% if is_last_question %}
<script>
document.addEventListener('DOMContentLoaded', () => {
const animationDuration = 1500; // Dauer der Wachstumsanimation
const waitTime=3000;
const podiums = [
{ id: 'podium-2', delay: 2000 }, // immer vorheriges von Bedeutung
{ id: 'podium-1', delay: 2000 },
{ id: 'podium-0', delay: 0 },
];
let totalDelay = 0;
podiums.forEach(({ delay }) => {
totalDelay += delay + waitTime;
});
const totalAnimationTime = totalDelay + animationDuration;
setTimeout(() => {
show_score();
}, totalAnimationTime);
totalDelay = 0;
podiums.forEach(({ id, delay }) => {
const podium = document.getElementById(id);
if (!podium) return;
const finalHeight = parseInt(podium.dataset.finalHeight) || 160;
podium.style.transition = `height ${animationDuration}ms ease-in-out`;
podium.style.height = '0px';
// Wachstum starten nach gesamter Wartezeit
setTimeout(() => {
podium.style.height = `${finalHeight}vh`;
}, totalDelay);
// Name & Score anzeigen nach Balken-Animation
setTimeout(() => {
if (id === 'podium-0') {
let name = document.getElementById(`display_name-0`);
let score = document.getElementById(`score-0`);
let winnerText= document.getElementById(`winner-text`);
let thanks= document.getElementById(`thanks`);
// 1. Lichtkegel anzeigen
if (resultFirst) {
resultFirst.classList.add('spotlight-wrapper');
score.classList.add('opacity-0');
name.classList.add('opacity-0');
}
// 2. Nach kurzer Wartezeit Name & Score anzeigen
setTimeout(() => {
if (name) {
name.classList.add('opacity-100', 'animate-bounce','spotlight-text'); // z.B. sichtbar + Effekt
}
if (winnerText) {
winnerText.classList.remove('hidden');
}
if (thanks) {
thanks.classList.remove('hidden');
}
if (score) {
score.classList.add('opacity-100'); // z.B. sichtbar + Effekt
}
triggerConfetti();
}, 2000);
document.body.style.overflow = 'auto';
return;
}
const index = id.split('-')[1];
const name = document.getElementById(`display_name-${index}`);
const score = document.getElementById(`score-${index}`);
document.body.style.overflow = 'hidden';
if (name && id !== 'podium-0') setTimeout(() => {
name.classList.add('opacity-100');
name.classList.add('animate-bounce');
if (score) score.classList.add('opacity-100');
}, waitTime);
}, totalDelay + animationDuration);
totalDelay += delay + waitTime; // wichtig: aufsummieren
});
});
// Hilfsfunktion für Pausen
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function triggerConfetti() {
const end = Date.now() + 30 * 1000;
(function frame() {
confetti({ particleCount: 2, angle: 60, spread: 55, origin: { x: 0 } });
confetti({ particleCount: 2, angle: 120, spread: 55, origin: { x: 1 } });
if (Date.now() < end) requestAnimationFrame(frame);
})();
}
function show_score() {
let player_score=document.getElementById('player_score');
player_score.classList.remove('hidden');
info=document.getElementById('info');
info.classList.add('hidden');
resultText=document.getElementById('result-text');
resultText.textContent="Dein Ergebnis";
const myName = "{{ my_participant.id|escapejs }}";
const firstPlaceName = "{{ participants.0.id|escapejs }}";
if (myName === firstPlaceName) {
let winnerText= document.getElementById(`winner-text`);
winnerText.classList.remove('hidden');
triggerConfetti();
}
}
</script>
{% endif %}
{% include 'play/game/common_scripts.html' %}
{% if is_host %}
{% include 'play/game/sound.html' %}
{% endif %}
<script>
async function start() {
elements=document.getElementsByClassName("show-score");
for (let i = 0; i < elements.length; i++) {
const el = elements[i];
let counter=Number(el.getAttribute('data-score'))-Number(el.getAttribute('data-last-score'));
let goal=Number(el.getAttribute('data-score'));
let interval= setInterval(function(){
el.innerHTML = counter + " Punkte";
if(counter<=goal){
el.innerHTML = counter + " Punkte";
counter+=8;
}
else{
el.innerHTML = goal + " Punkte";
clearInterval(interval);
}
}, 20);}}
function bounce(){
const names = document.getElementsByClassName("display_name");
for (let i = 0; i < names.length; i++) {
names[i].classList.add("animate-bounce");
}
}
{% if is_last_question %}
start();
bounce();
{% else %}
start();
{% endif %}
const richtigeAntworten = [
"Deine Antwort ist nicht falsch. Sie ist richtig!",
"Ja, du kannst es!",
"So kann es weiter gehen.",
"Ja, deine Antwort ist korrekt. Was soll ich dazu noch sagen? SUPER!",
"SUPER GUT.",
"Volltreffer!",
"Bravo! Das sitzt!",
"Na bitte, geht doch!",
"Schön. Einfach schön.",
"Exzellent kombiniert, Sherlock!",
"Punkt für dich!",
"Das war meisterhaft.",
"Korrekt - du scheinst das zu können!",
"So sieht Erfolg aus!",
"Glänzend gelöst!",
"Das war keine Glückssache - das war Können!",
"Treffer, versenkt!",
"Du hast es drauf!",
"Da merkt man: Profi am Werk.",
"Richtige Antwort - dafür gibt es viele Punkte!",
"Weiter so!",
"Wow - das war richtig gut!"
];
const falscheAntworten = [
"Oops! War wohl nicht dein Tag.",
"Knapp daneben ist auch vorbei!",
"Das war... kreativ!",
"Wenn man schon alles wüsste, wäre das Quiz doch sinnlos.",
"Deine Antwort war einzigartig - leider falsch!",
"Na gut, wenigstens hast du es probiert.",
"Das war eine interessante Theorie...",
"Ein Genie irrt sich auch mal.",
"Eine gute Antwort - aber nicht auf diese Frage.",
"Das ist leider falsch. Mehr gibt es nicht zu sagen.",
"Gute Idee! Aber leider nicht richtig.",
"Oh weh! Ein Fehler. Naja, einer ist keiner.",
"Die Antwort klang richtig, sie ist es aber nicht.",
"Interessanter Ansatz. Nicht hilfreich, aber interessant.",
"Nenn das lieber „alternative Fakten“.",
"Du hast die richtige Antwort doch gewusst: 'Die Seite hat nur nicht geladen.'",
"Jeder fängt mal klein an!",
"Kopf hoch - weiter geht's!",
"Nobody is perfect!",
"Du schaffst das - vielleicht ab jetzt.",
"Falsche Antwort, aber guter Wille!",
"Übung macht den Quiz-Meister.",
"Beim nächsten Mal triffst du!",
"Das war nur zum Aufwärmen, oder?",
"Kein Problem - weiter geht's.",
"Gute Frage - aber falsche Antwort."
];
const antwortRichtig = document.getElementById("antwort-richtig");
const antwortFalsch = document.getElementById("antwort-falsch");
if (antwortRichtig) {
antwortRichtig.textContent = richtigeAntworten[Math.floor(Math.random() * richtigeAntworten.length)];
} else if (antwortFalsch) {
antwortFalsch.textContent = falscheAntworten[Math.floor(Math.random() * falscheAntworten.length)];
}
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' || data.action === 'show_winner_podest')) {
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
}));
});
}
var podestButton = document.getElementById('winner-button');
if (podestButton) {
podestButton.addEventListener('click', function() {
gameSocket.send(JSON.stringify({
'type': 'winner_podest',
'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
}));
}, { once: true });
}
{% endif %}
{% endif %}
function updateAnswerStats(stats) {
const total = Object.values(stats).reduce((a, b) => a + b, 0) || 1;
for (var optionIndex in stats) {
if (stats.hasOwnProperty(optionIndex)) {
var countElement = document.getElementById('option-count-' + optionIndex);
var fillElement = document.getElementById('option-fill-' + optionIndex);
if (countElement) {
countElement.textContent = stats[optionIndex] + ' Antwort(en)';
}
if (fillElement) {
const percentage = Math.round((stats[optionIndex] / total) * 100);
fillElement.style.width = percentage + '%';
}
}
}
}
// Request answer stats when page loads
gameSocket.onopen = function(e) {
gameSocket.send(JSON.stringify({
'type': 'get_answer_stats'
}));
};
</script>
{% endblock %}