Compare commits

...

19 Commits

Author SHA1 Message Date
ben8
e84c13c40a settings 2025-06-01 18:27:06 +02:00
ben8
3bfca331f2 Merge branch '127-suchfunktion-verbessern' into 'master'
Resolve "Suchfunktion verbessern"

Closes #127

See merge request enrichment-2024/qivip!85
2025-06-01 14:16:49 +00:00
ben8
dcee37f295 Optimierte Suchvorschläge 2025-06-01 16:16:25 +02:00
ben8
78bf2d296f Merge branch '126-kein-zugriff-auch-privates-quiz' into 'master'
Resolve "kein Zugriff auch privates Quiz"

Closes #126

See merge request enrichment-2024/qivip!84
2025-06-01 13:54:39 +00:00
ben8
a78cbcc10e privates Quiz sichern 2025-06-01 15:54:15 +02:00
ben8
c245f42821 Merge branch 'master' of edugit.org:enrichment-2024/qivip 2025-05-23 19:39:14 +02:00
ben8
0c8223ec83 maximale Bildgröße 2025-05-23 19:39:10 +02:00
ben8
3056a8b389 Merge branch '125-counter-der-punkte-js-animation' into 'master'
Resolve "Counter der Punkte (js animation)"

Closes #125

See merge request enrichment-2024/qivip!83
2025-05-23 17:24:36 +00:00
ben8
f30c984342 counter für Punkte und Optikverbesserungen 2025-05-23 19:22:41 +02:00
ben8
8f6bfd2aad QR Code Größe 2025-05-19 18:42:39 +02:00
ben8
2268035e58 Merge branch '124-qr-code' into 'master'
Resolve "QR Code"

Closes #124

See merge request enrichment-2024/qivip!82
2025-05-19 16:31:54 +00:00
ben8
59c3e5d90c QR Code 2025-05-19 18:31:17 +02:00
ben8
8efd22034c Merge branch '123-reaktion-auf-die-antworten-3' into 'master'
Resolve "Reaktion auf die antworten"

Closes #123

See merge request enrichment-2024/qivip!81
2025-05-18 17:54:09 +00:00
ben8
d02109e474 Sprüche nach Antwort 2025-05-18 19:53:33 +02:00
Frank Poetzsch-Heffter
49f5e0bad9 python3 core/manage.py to python django/manage.py 2025-05-18 09:35:58 +00:00
Frank Poetzsch-Heffter
321487e7c8 Merge branch '120-produktivumgebung-vorbereiten' into 'master'
Resolve "Produktivumgebung vorbereiten"

Closes #120

See merge request enrichment-2024/qivip!77
2025-05-18 09:23:25 +00:00
Frank Poetzsch-Heffter
b318237f50 dotenv eingerichtet 2025-05-18 11:18:51 +02:00
nik8
198905232c Merge branch '110-hintergrundmusik' into 'master'
Resolve "Hintergrundmusik?"

Closes #110

See merge request enrichment-2024/qivip!76
2025-05-17 12:57:58 +00:00
nik8
43f81a5ae7 Musik 2025-05-17 14:55:41 +02:00
20 changed files with 295 additions and 38 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
__pycache__ __pycache__
.venv .venv
.env
db.sqlite3 db.sqlite3
tailwindcss.exe tailwindcss.exe
t-style.css t-style.css

View File

@@ -41,7 +41,7 @@ Um das Projekt erfolgreich zu starten, folge diesen Schritten:
``` ```
7. Die Datenbank migrieren: 7. Die Datenbank migrieren:
```sh ```sh
python3 core/manage.py migrate python django/manage.py migrate
``` ```
## Starten des Servers ## Starten des Servers

View File

@@ -10,8 +10,9 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/ https://docs.djangoproject.com/en/5.1/ref/settings/
""" """
import json import json, os
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@@ -21,11 +22,17 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' #TODO: NUR FÜR
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# Support env variables from .env file if defined
env_path = load_dotenv(os.path.join(BASE_DIR, '.env'))
load_dotenv(env_path)
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-#+s307$rxkts=8@+_^i)8)n1b4*y4za1xz!mvv(h@!+n9!mp9)' #SECRET_KEY = 'django-insecure-#+s307$rxkts=8@+_^i)8)n1b4*y4za1xz!mvv(h@!+n9!mp9)'
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'django-insecure-&psk#na5l=p3q8_a+-$4w1f^lt3lx1c@d*p4x$ymm_rn7pwb87')
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True #DEBUG = True
DEBUG = os.environ.get('DJANGO_DEBUG', '') != 'False'
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '*'] ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '*']
@@ -152,7 +159,7 @@ STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_DIRS = [BASE_DIR / 'static'] STATICFILES_DIRS = [BASE_DIR / 'static']
# WhiteNoise configuration # WhiteNoise configuration
STATICFILES_STORAGE = 'whitenoise.storage.CompressedStaticFilesStorage' STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
WHITENOISE_SKIP_COMPRESS_EXTENSIONS = ['css', 'js'] WHITENOISE_SKIP_COMPRESS_EXTENSIONS = ['css', 'js']
# Return 404 instead of 500 for missing files # Return 404 instead of 500 for missing files
WHITENOISE_MISSING_FILE_ERRNO = None WHITENOISE_MISSING_FILE_ERRNO = None

View File

@@ -21,11 +21,21 @@ def quiz_names_json(request):
form = QuizFilterForm(request.GET) form = QuizFilterForm(request.GET)
if form.is_valid(): if form.is_valid():
search = form.cleaned_data.get('search') search = form.cleaned_data.get('search')
names = list(
QivipQuiz.objects.all().annotate(max_amout_questions=Count('questions')).filter(max_amout_questions__gte=1, status='öffentlich', name__icontains=search)
.values_list('name', flat=True) names = QivipQuiz.objects.all().annotate(max_amout_questions=Count('questions')).filter(max_amout_questions__gte=1, status='öffentlich', name__icontains=search)
.distinct()
)
if request.user.is_authenticated:
names = names | QivipQuiz.objects.filter(
user_id=request.user.id,
status='privat',
name__icontains=search
)
names=list(names.values_list('name', flat=True).distinct())
return JsonResponse(names, safe=False) return JsonResponse(names, safe=False)
def overview_quiz(request): def overview_quiz(request):
@@ -260,7 +270,6 @@ def delete_quiz(request, pk):
# Quiz anzeigen # Quiz anzeigen
def detail_quiz(request, pk): def detail_quiz(request, pk):
#quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
quiz = get_object_or_404(QivipQuiz, pk=pk) quiz = get_object_or_404(QivipQuiz, pk=pk)
show_answers = request.GET.get('show_answers', 'false').lower() == 'true' show_answers = request.GET.get('show_answers', 'false').lower() == 'true'
questions = QivipQuestion.objects.filter(quiz_id=quiz) questions = QivipQuestion.objects.filter(quiz_id=quiz)
@@ -274,7 +283,11 @@ def detail_quiz(request, pk):
'questions': questions, 'questions': questions,
'detail':show_answers, 'detail':show_answers,
} }
return render(request, 'library/detail_quiz.html', context) if quiz.status!="privat" or quiz.user_id==request.user:
return render(request, 'library/detail_quiz.html', context)
else:
return redirect('library:overview_quiz')
@login_required @login_required

View File

@@ -129,6 +129,7 @@ class GameConsumer(AsyncWebsocketConsumer):
time_factor = time_remaining / 30000 # 30 seconds max time_factor = time_remaining / 30000 # 30 seconds max
score = int(base_score * (0.5 + 0.5 * time_factor)) score = int(base_score * (0.5 + 0.5 * time_factor))
participant.last_score=score
participant.score += score participant.score += score
participant.last_answer_correct = is_correct participant.last_answer_correct = is_correct
participant.save() participant.save()
@@ -460,7 +461,7 @@ class LobbyConsumer(AsyncWebsocketConsumer):
if answer == correct_answer: if answer == correct_answer:
# Base score for correct answer + bonus for speed # Base score for correct answer + bonus for speed
score = 1000 + int(time_remaining * 10) # 10 points per remaining second score = 1000 + int(time_remaining * 10) # 10 points per remaining second
participant.last_score= score
participant.score += score participant.score += score
participant.save() participant.save()
return score return score

View File

@@ -233,6 +233,7 @@ class GameConsumer(AsyncWebsocketConsumer):
answer_obj.time_remaining = time_remaining answer_obj.time_remaining = time_remaining
answer_obj.save() answer_obj.save()
participant.last_score = score
participant.score += score participant.score += score
participant.last_answer_correct = is_correct participant.last_answer_correct = is_correct
participant.save() participant.save()

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-05-22 15:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0011_quizgame_updated_at'),
]
operations = [
migrations.AddField(
model_name='quizgameparticipant',
name='last_score',
field=models.IntegerField(default=0, verbose_name='Letzte_Punkte'),
),
]

View File

@@ -43,6 +43,7 @@ class QuizGameParticipant(models.Model):
display_name = models.CharField(verbose_name="Anzeigename", max_length=15) display_name = models.CharField(verbose_name="Anzeigename", max_length=15)
quiz_game = models.ForeignKey(QuizGame, on_delete=models.CASCADE, related_name="participants") quiz_game = models.ForeignKey(QuizGame, on_delete=models.CASCADE, related_name="participants")
score = models.IntegerField(verbose_name="Punkte", default=0) score = models.IntegerField(verbose_name="Punkte", default=0)
last_score = models.IntegerField(verbose_name="Letzte_Punkte", default=0)
avatar = models.CharField(max_length=200, blank=True, default="") avatar = models.CharField(max_length=200, blank=True, default="")
last_heartbeat = models.DateTimeField(auto_now_add=True) last_heartbeat = models.DateTimeField(auto_now_add=True)
last_answer = models.IntegerField(null=True, blank=True) last_answer = models.IntegerField(null=True, blank=True)

View File

@@ -8,10 +8,20 @@ from django.contrib import messages
from django.conf import settings from django.conf import settings
import json import json
import qrcode
import base64
from io import BytesIO
from django.shortcuts import render
# Create your views here. # Create your views here.
def lobby(request, join_code): def lobby(request, join_code):
relative_url = reverse("play:create_participant", kwargs={"join_code": join_code})
full_url = request.build_absolute_uri(relative_url)
qr = qrcode.make(full_url)
buffer = BytesIO()
qr.save(buffer, format="PNG")
img_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
@@ -20,6 +30,7 @@ def lobby(request, join_code):
context = { context = {
'quiz': quiz, 'quiz': quiz,
'quiz_game': quiz_game, 'quiz_game': quiz_game,
"qr_code_base64": img_base64,
} }
@@ -29,6 +40,7 @@ def lobby(request, join_code):
host_id = request.COOKIES['host_id'] host_id = request.COOKIES['host_id']
if host_id == quiz_game.host_id: if host_id == quiz_game.host_id:
context['host_id'] = host_id context['host_id'] = host_id
context["qr_code_base64"] = img_base64
return render(request, 'play/lobby.html', context=context) return render(request, 'play/lobby.html', context=context)
elif mode=="learn": elif mode=="learn":
return redirect('play:create_participant', join_code=join_code) return redirect('play:create_participant', join_code=join_code)
@@ -45,11 +57,13 @@ def lobby(request, join_code):
if not participant.quiz_game.join_code == join_code: # Teilnehmer dem richtigen Spiel hinzufügen if not participant.quiz_game.join_code == join_code: # Teilnehmer dem richtigen Spiel hinzufügen
participant.quiz_game = quiz_game participant.quiz_game = quiz_game
participant.score = 0 # Reset score when joining new game participant.score = 0 # Reset score when joining new game
participant.last_score = 0
participant.save() participant.save()
except QuizGameParticipant.DoesNotExist: except QuizGameParticipant.DoesNotExist:
return redirect('play:create_participant', join_code=join_code) return redirect('play:create_participant', join_code=join_code)
context['participant'] = participant context['participant'] = participant
context["qr_code_base64"] = img_base64
return render(request, 'play/lobby.html', context=context) return render(request, 'play/lobby.html', context=context)
def select_mode(request,join_code): def select_mode(request,join_code):
@@ -70,6 +84,7 @@ def create_participant(request, join_code):
if participant.quiz_game.join_code != join_code: if participant.quiz_game.join_code != join_code:
participant.quiz_game = quiz_game participant.quiz_game = quiz_game
participant.score = 0 # Reset score when joining new game participant.score = 0 # Reset score when joining new game
participant.last_score = 0
participant.save() participant.save()
return redirect('play:lobby', join_code=join_code) return redirect('play:lobby', join_code=join_code)
except QuizGameParticipant.DoesNotExist: except QuizGameParticipant.DoesNotExist:
@@ -90,6 +105,7 @@ def create_participant(request, join_code):
participant.display_name = display_name participant.display_name = display_name
participant.quiz_game = quiz_game participant.quiz_game = quiz_game
participant.score = 0 # Initialize score participant.score = 0 # Initialize score
participant.last_score = 0
participant.save() participant.save()
response = HttpResponseRedirect(reverse('play:lobby', kwargs={'join_code': join_code})) response = HttpResponseRedirect(reverse('play:lobby', kwargs={'join_code': join_code}))
@@ -327,6 +343,7 @@ def selected_mode(request, join_code):
participant.participant_id=host_id participant.participant_id=host_id
participant.quiz_game = quiz_game participant.quiz_game = quiz_game
participant.score = 0 # Initialize score participant.score = 0 # Initialize score
participant.last_score = 0
participant.save() participant.save()
quiz_game.current_state = "question" quiz_game.current_state = "question"
quiz_game.question_start_time = timezone.now() quiz_game.question_start_time = timezone.now()
@@ -334,5 +351,5 @@ def selected_mode(request, join_code):
return redirect('play:question',join_code) return redirect('play:question',join_code)
return redirect('play:lobby', join_code=join_code)
return render(request, 'play/lobby.html', context=context)

View File

Binary file not shown.

View File

@@ -68,7 +68,7 @@
<br> <br>
- alle öffentlichen Quizze können auch ohne Account gespielt werden - alle öffentlichen Quizze können auch ohne Account gespielt werden
<br> <br>
- qivip ist 100% kostenlos - qivip ist zu 100% kostenlos
<br> <br>
- es gibt keine Werbung - es gibt keine Werbung
<br> <br>
@@ -133,7 +133,7 @@
<br> <br>
<br> <br>
<b>Moderator: </b>Der Moderator selbst spielt nicht mit. Gehen Sie entweder auf die Startseite und klicken Sie auf "Moderieren" oder klicken einfach auf "spielen" (eines von Ihnen ausgewählten Quizzes) und dann auf Moderiermodus. <b>Moderator: </b>Der Moderator selbst spielt nicht mit. Gehen Sie entweder auf die Startseite und klicken Sie auf "Moderieren" oder klicken einfach auf "spielen" (eines von Ihnen ausgewählten Quizzes) und dann auf Moderiermodus.
Der Moderator kann das Spiel, wenn die Teilnehmer da sind starten. Der Moderator kann das Spiel, wenn die Teilnehmer da sind, starten.
<br> <br>
<br> <br>
<b>Teilnehmer: </b>Gehen Sie auf die Startseite und klicken Sie auf "Teilnehmen" und geben Sie dann den beim Moderator angegebenen Zahlencode ein. Anschließlich können Sie noch einen Anzeigenamen eingeben. <b>Teilnehmer: </b>Gehen Sie auf die Startseite und klicken Sie auf "Teilnehmen" und geben Sie dann den beim Moderator angegebenen Zahlencode ein. Anschließlich können Sie noch einen Anzeigenamen eingeben.

View File

@@ -25,7 +25,7 @@
</div> </div>
{% if image %} {% if image %}
<img src="{{ image.image.url }}" style="max-width: 500px;" alt="Bild der Frage"> <img src="{{ image.image.url }}" style="max-width: 100px;" alt="Bild der Frage">
<label for="reset_image"> <label for="reset_image">
<input type="checkbox" name="reset_ques_image" id="reset_ques_image"> <input type="checkbox" name="reset_ques_image" id="reset_ques_image">
Bild zurücksetzen Bild zurücksetzen

View File

@@ -25,7 +25,7 @@
{% if image %} {% if image %}
<img src="{{ image.image.url }}" style="max-width: 500px;" alt="Bild der Frage"> <img src="{{ image.image.url }}" style="max-width: 100px;" alt="Bild der Frage">
<label for="reset_image"> <label for="reset_image">
<input type="checkbox" name="reset_ques_image" id="reset_ques_image"> <input type="checkbox" name="reset_ques_image" id="reset_ques_image">
Bild zurücksetzen Bild zurücksetzen

View File

@@ -25,7 +25,7 @@
</div> </div>
{% if image %} {% if image %}
<img src="{{ image.image.url }}" style="max-width: 500px;" alt="Bild der Frage"> <img src="{{ image.image.url }}" style="max-width: 100px;" alt="Bild der Frage">
<label for="reset_image"> <label for="reset_image">
<input type="checkbox" name="reset_ques_image" id="reset_ques_image"> <input type="checkbox" name="reset_ques_image" id="reset_ques_image">
Bild zurücksetzen Bild zurücksetzen

View File

@@ -15,7 +15,7 @@
{% if question_image %} {% if question_image %}
<div class="flex justify-center"> <div class="flex justify-center">
<img class="m-4" src="{{ question_image.image.url }}" style="max-width: 500px;" alt="Bild der Frage"> <img class="m-4 w-full max-w-[500px] " src="{{ question_image.image.url }}" alt="Bild der Frage">
</div> </div>
{% endif %} {% endif %}
@@ -73,6 +73,7 @@
{% block extra_js %} {% block extra_js %}
{% include 'play/game/common_scripts.html' %} {% include 'play/game/common_scripts.html' %}
{% include 'play/game/sound.html' %}
<script> <script>
const joinCode = '{{ quiz_game.join_code }}'; const joinCode = '{{ quiz_game.join_code }}';
const hostId = '{{ host_id }}'; const hostId = '{{ host_id }}';

View File

@@ -24,32 +24,120 @@
</div> </div>
</div> </div>
<div class="mb-6"> <div class="mb-6">
<h2 class="text-xl font-bold mb-4">Punktestand</h2> <h2 class="text-xl font-bold mb-4">Punktestand</h2>
<div id="scoreboard" class="space-y-2">
<div id="scoreboard" class="leading-relaxed">
{% for participant in participants %} {% for participant in participants %}
{% if participant == my_participant or is_host %} {% if participant == my_participant or is_host %}
<div class="p-4 bg-gray-100 rounded-lg flex justify-between items-center border {% if forloop.counter <= 3 %}border-yellow-500{% else %}border-transparent{% endif %}"> <div class="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> <div class="flex flex-col items-start gap-1">
<p class="text-lg font-bold"> <div class="text-lg font-bold flex items-center gap-2">
{% if forloop.counter == 1 %} {% if forloop.counter == 1 %}
🥇 🥇
{% elif forloop.counter == 2 %} {% elif forloop.counter == 2 %}
🥈 🥈
{% elif forloop.counter == 3 %} {% elif forloop.counter == 3 %}
🥉 🥉
{% else %}
{{ forloop.counter }}.
{% endif %} {% endif %}
{{ participant.display_name }} <span class="display_name text-gray-600 font-black text-2xl " id="display_name-{{ forloop.counter0 }}">{{ participant.display_name }}</span>
</p> </div class="flex flex-col items-start">
<p data-last-score="{{ participant.last_score }}" class="text-gray-600" id="score-{{ forloop.counter0 }}">{{ participant.score }} Punkte</p>
<p data-score="{{ participant.score }}" data-last-score="{{ participant.last_score }}" class="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 %}
</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>
{% if participant == my_participant or request.session.mode == "learn" %}
{% if participant.last_answer_correct %}
<div class="mt-1 flex justify-center border-t-2 border-gray-300 gap-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="pt-2 text-center text-green-500 font-black size-10">
<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>
<div class=" pt-3 text-center text-gray-600 font-bold" id="antwort-richtig">
Hier erscheint gleich dein Feedback...
</div>
</div> </div>
{% if participant.last_answer_correct %}
<span class="text-green-500"></span>
{% elif participant.last_answer_correct == False %} {% elif participant.last_answer_correct == False %}
<span class="text-red-500"></span>
<div class="mt-1 flex justify-center border-t-2 border-gray-300 gap-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="pt-2 text-center text-red-500 font-black size-10">
<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>
<div class=" pt-3 text-center text-gray-600 font-bold" id="antwort-falsch">
Hier erscheint gleich dein Feedback...
</div>
<div class="flex justify-center border-t-2 border-gray-500 gap-1">
{% endif %}
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>
@@ -57,11 +145,11 @@
{% if is_host %} {% if is_host %}
{% if is_last_question %} {% 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"> <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 Quiz beenden
</button> </button>
{% else %} {% 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"> <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 Nächste Frage
</button> </button>
{% endif %} {% endif %}
@@ -95,7 +183,8 @@
}); });
// Warten bevor Animation startet // Warten bevor Animation startet
await sleep(1500); // <<< hier die 5 Sekunden Pause await sleep(1500); // <<< hier die Pause
// Transition-Eigenschaft setzen nach Render-Zyklus // Transition-Eigenschaft setzen nach Render-Zyklus
requestAnimationFrame(() => { requestAnimationFrame(() => {
@@ -111,6 +200,7 @@
if (index === top3.length - 1) { if (index === top3.length - 1) {
triggerConfetti(); triggerConfetti();
} }
}, index * 1500); // Zeitversetzt aufdecken }, index * 1500); // Zeitversetzt aufdecken
}); });
@@ -128,7 +218,78 @@
</script> </script>
{% endif %} {% endif %}
{% include 'play/game/common_scripts.html' %} {% include 'play/game/common_scripts.html' %}
{% if is_host %}
{% include 'play/game/sound.html' %}
{% endif %}
<script> <script>
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 joinCode = '{{ quiz_game.join_code }}';
const hostId = '{{ host_id }}'; const hostId = '{{ host_id }}';
const wsScheme = window.location.protocol === 'https:' ? 'wss' : 'ws'; const wsScheme = window.location.protocol === 'https:' ? 'wss' : 'ws';

View File

@@ -0,0 +1,29 @@
{% load static %}
<audio id="bg-music" src="{% static 'mp3/Hintergrundmusik.mp3' %}" autoplay loop ></audio>
<script>
document.addEventListener("DOMContentLoaded", function () {
const audio = document.getElementById("bg-music");
const savedTime = localStorage.getItem("bg-music-time");
if (savedTime) {
audio.addEventListener("loadedmetadata", () => {
audio.currentTime = parseFloat(savedTime);
});
}
document.addEventListener("click", () => {
audio.play().catch(e => console.warn("Autoplay blockiert:", e));
}, { once: true });
setInterval(() => {
if (!audio.paused) {
localStorage.setItem("bg-music-time", audio.currentTime);
}
}, 0);
});
</script>

View File

@@ -8,12 +8,17 @@
<div class="text-center mb-8"> <div class="text-center mb-8">
<h3 class="text-xl text-gray-600 mb-2">{{ quiz.name }}</h3> <h3 class="text-xl text-gray-600 mb-2">{{ quiz.name }}</h3>
{% if request.session.mode != "learn" %} {% if request.session.mode != "learn" %}
{% if host_id %}
<div class="bg-blue-100 rounded-lg p-4 mb-4"> <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> <h1 class="text-4xl font-bold text-blue-800">{{ quiz_game.join_code }}</h1>
<div class="w-full flex justify-center my-2">
<img src="data:image/png;base64,{{ qr_code_base64 }}" class="h-1/2 w-1/2" alt="QR-Code">
</div>
<p class="text-sm text-blue-600 mt-1">Spiel-Code</p> <p class="text-sm text-blue-600 mt-1">Spiel-Code</p>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endif %}
<!-- Participant Info --> <!-- Participant Info -->
{% if participant %} {% if participant %}

View File

@@ -7,3 +7,5 @@ whitenoise~=6.6.0 # Static file serving
django-cleanup django-cleanup
redis redis
channels_redis channels_redis
python-dotenv # Production server safety
qrcode~=7.4.2