Compare commits

..

26 Commits

Author SHA1 Message Date
ben8
a7c42ecf13 Merge branch '108-bilderupload-verbessern' into 'master'
Resolve "Bilderupload verbessern!"

Closes #108

See merge request enrichment-2024/qivip!60
2025-04-23 15:30:51 +00:00
ben8
cc8cc9ad74 Bild_löschen 2025-04-23 16:13:14 +02:00
ben8
0c8a8d3062 sinnvolleres_speichern 2025-04-23 16:11:08 +02:00
ben8
a021e7561a Bildspeichern_DB_fix 2025-04-23 13:02:28 +02:00
ben8
9321b44056 Bild_anzeige_in_modi_fix 2025-04-23 12:16:18 +02:00
Juhn-Vitus Saß
873b50103d Revert breaking changes.
Revert "Add missing file for collect static"

This reverts commit b823909386.
2025-04-23 12:00:50 +02:00
ben8
41cecf98dc fix_Bilder 2025-04-23 11:52:32 +02:00
Juhn-Vitus Saß
ac5859a666 Move Media Root Definition 2025-04-23 10:57:14 +02:00
Juhn-Vitus Saß
779762c2a3 WhiteNoise Configuration 2025-04-23 10:50:39 +02:00
Juhn-Vitus Saß
da78060415 Serve media in Production 2025-04-23 10:45:15 +02:00
Juhn-Vitus Saß
b823909386 Add missing file for collect static 2025-04-22 22:41:40 +02:00
Juhn-Vitus Saß
82f0cd5191 Correct Package name 2025-04-22 20:05:11 +02:00
Juhn-Vitus Saß
1069dd1cad Nachricht für keine Bewertung auch bei fremden Quizzen anzeigen 2025-04-22 19:36:16 +02:00
Juhn-Vitus Saß
905da6737c Merge branch '89-pop-up-menu-fur-mobile' into 'master'
Resolve "Pop-up Menü für Mobile"

Closes #89

See merge request enrichment-2024/qivip!64
2025-04-22 17:14:14 +00:00
Juhn-Vitus Saß
6eed8107ac feat(navbar): Implementiere responsive Navbar mit mobilem Menü
- Füge Hamburger-Menü für mobile Ansichten hinzu
- Positioniere Suchleiste zentral auf dem Desktop
- Implementiere animierte Transitions beim Öffnen/Schließen des Menüs
- Entferne untere Border-Radius der Navbar bei geöffnetem Menü für nahtloses Erscheinungsbild
- Optimiere Layout für verschiedene Bildschirmgrößen
- Behalte Designsprache und Farbschema bei (blau/weiß)
2025-04-22 19:13:55 +02:00
ben8
95e4428943 fix_Bilder 2025-04-22 19:11:31 +02:00
Juhn-Vitus Saß
bad257f341 Merge branch '83-auch-bei-dem-orginalquiz-sollte-der-autor-stehen' into 'master'
Resolve "Auch bei dem Orginalquiz sollte der Autor stehen!"

Closes #83

See merge request enrichment-2024/qivip!63
2025-04-22 16:55:42 +00:00
Juhn-Vitus Saß
eaf709ed4d Verbesserte Ansicht des Ursprungs und der Editoren 2025-04-22 18:54:59 +02:00
Juhn-Vitus Saß
8ca470ad54 Merge branch '71-bewertungssystem-ueberarbeiten' into 'master'
Resolve "Bewertungssystem ueberarbeiten"

Closes #71

See merge request enrichment-2024/qivip!62
2025-04-22 16:33:02 +00:00
Juhn-Vitus Saß
af919b9ea5 README updaten 2025-04-22 18:31:51 +02:00
Juhn-Vitus Saß
154110e510 Bewertungen verstecken wenn keine vorhanden und Möglichkeit zum deaktivieren von Bewertungen 2025-04-22 18:28:50 +02:00
Juhn-Vitus Saß
a16c97c4f3 Merge branch '70-siegerpodest-am-spielende' into 'master'
Resolve "Siegerpodest am Spielende"

Closes #70 and #72

See merge request enrichment-2024/qivip!61
2025-04-22 15:57:55 +00:00
Juhn-Vitus Saß
01562c6ad8 Konfetti animation
Fix #72
2025-04-22 17:57:27 +02:00
ben8
50cd95daf1 Uploadfunktion! 2025-04-22 17:52:34 +02:00
ben8
1a4b9e0d6b Hübscheres_Bilder_Upload+Zurücksetzfunktion 2025-04-22 17:51:19 +02:00
Juhn-Vitus Saß
867c839202 Abhängigkeiten erneuern 2025-04-22 14:24:26 +02:00
24 changed files with 534 additions and 96 deletions

View File

@@ -7,6 +7,7 @@
3. [Superuser erstellen](#superuser-erstellen)
4. [Tailwind-Nutzung](#tailwind-nutzung)
5. [Issue - Merge Request - Merge](#issue-mergerequest-merge)
6. [Konfiguration](#konfiguration)
## Initialisierung des Projekts
@@ -161,3 +162,11 @@ Hier der vereinbarte Arbeitsablauf:
2. Merge Request anlegen - dadurch wird der Branch automatisch angelegt. Dieser kann mit `git pull` geholt und mit `git checkout <branch>` bearbeitet werden.
3. Nach dem Hochladen kann der Merge in den Master durchgeführt werden.
## Konfiguration
Die Konfiguration des Projekts befindet sich im `config/qivip_config.json`.
| Variable | Bedeutung | Optionen |
| --- | --- | --- |
| ENABLE_RATING_SYSTEM | Soll das Bewertungssystem aktiviert werden? | true [Standard], false |

View File

@@ -0,0 +1,3 @@
{
"ENABLE_RATING_SYSTEM": true
}

View File

@@ -10,6 +10,7 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
import json
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
@@ -27,6 +28,9 @@ DEBUG = True
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '*']
# Konfigurationsdatei für Projekt
with open(BASE_DIR / 'config' / 'qivip_config.json') as config_file:
QIVIP_CONFIG = json.load(config_file)
# Application definition

View File

@@ -1,5 +1,5 @@
from django.contrib import admin
from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite, QuizCategory
from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite, QuestionImage, QuizCategory, QuizImage
# Für das QivipQuiz Modell
class QivipQuizAdmin(admin.ModelAdmin):
@@ -23,8 +23,17 @@ class QuizCategoryAdmin(admin.ModelAdmin):
class QivipQuizFavoriteAdmin(admin.ModelAdmin):
list_display = ('user', 'favorite', 'quiz') # Welche Felder sollen in der Übersicht angezeigt werden
class QuizImageAdmin(admin.ModelAdmin):
list_display = ('image',)
class QuestionImageAdmin(admin.ModelAdmin):
list_display = ('image',)
# Registrierung der Modelle im Admin
admin.site.register(QivipQuiz, QivipQuizAdmin)
admin.site.register(QivipQuestion, QivipQuestionAdmin)
admin.site.register(QuizCategory, QuizCategoryAdmin)
admin.site.register(QivipQuizFavorite, QivipQuizFavoriteAdmin)
admin.site.register(QuizImage, QuizImageAdmin)
admin.site.register(QuestionImage, QuestionImageAdmin)

View File

@@ -4,7 +4,7 @@ from .models import QivipQuiz, QivipQuestion
class QuizForm(forms.ModelForm):
class Meta:
model = QivipQuiz
fields = ['name', 'description','image', 'status', 'category','difficulty','credits']
fields = ['name', 'description', 'status', 'category','difficulty','credits']
class QuestionForm(forms.ModelForm):
class Meta:

View File

@@ -0,0 +1,38 @@
# Generated by Django 5.1.7 on 2025-04-22 16:04
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0049_alter_qivipquestion_image'),
]
operations = [
migrations.CreateModel(
name='QuestionImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(blank=True, null=True, upload_to='question_images/', verbose_name='Bild')),
],
),
migrations.CreateModel(
name='QuizImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/', verbose_name='Bild')),
],
),
migrations.AlterField(
model_name='qivipquestion',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='questions', to='library.questionimage'),
),
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quizzes', to='library.QuizImage'),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 5.1.7 on 2025-04-23 12:04
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0050_questionimage_quizimages_alter_qivipquestion_image_and_more'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='library.questionimage'),
),
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='quizzes', to='library.quizimage'),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 5.1.7 on 2025-04-23 12:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0051_alter_qivipquestion_image_alter_qivipquiz_image'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='question_images/', verbose_name='Bild'),
),
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/', verbose_name='Bild'),
),
migrations.DeleteModel(
name='QuestionImage',
),
migrations.DeleteModel(
name='QuizImage',
),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 5.1.7 on 2025-04-23 13:15
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0052_alter_qivipquestion_image_alter_qivipquiz_image_and_more'),
]
operations = [
migrations.CreateModel(
name='QuestionImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(blank=True, null=True, upload_to='question_images/', verbose_name='Bild')),
],
),
migrations.CreateModel(
name='QuizImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/', verbose_name='Bild')),
],
),
migrations.AlterField(
model_name='qivipquestion',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='questions', to='library.questionimage'),
),
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='quizzes', to='library.quizimage'),
),
]

View File

@@ -4,6 +4,15 @@ from django.utils.text import slugify
import uuid
from django.core.exceptions import ValidationError
class QuizImage(models.Model):
image = models.ImageField(max_length=256,verbose_name="Bild", upload_to='quiz_images/', blank=True, null=True) # Bild speichern in media/quiz_images/
class QuestionImage(models.Model):
image = models.ImageField(verbose_name="Bild", upload_to="question_images/", blank=True, null=True)
class QivipQuiz(models.Model):
STATUS_VALUES = {
"öffentlich": "Öffentlich",
@@ -27,7 +36,15 @@ class QivipQuiz(models.Model):
if value.strip() == "In dem Quiz geht es um ...":
raise ValidationError("Bitte gib eine aussagekräftige Beschreibung ein.")
image = models.ImageField(max_length=256,verbose_name="Bild", upload_to='quiz_images/', blank=True, null=True) # Bild speichern in media/quiz_images/
image = models.ForeignKey(
QuizImage,
null=True,
blank=True,
on_delete=models.PROTECT,
related_name="quizzes"
)
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
user_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='quiz')
#creator = models.ForeignKey(User, on_delete=models.SET_NULL,blank=True, null=True, related_name='creator_quiz')
@@ -74,7 +91,14 @@ class QivipQuestion(models.Model):
update_date = models.DateTimeField(auto_now=True)
data = models.TextField()
time_per_question = models.CharField(max_length=3, choices=list(TIME_VALUES.items()), default=30, help_text="Zeit pro Frage in Sekunden")
image = models.ImageField(verbose_name="Bild", upload_to="question_images/", blank=True, null=True)
image = models.ForeignKey(
QuestionImage,
null=True,
blank=True,
on_delete=models.PROTECT,
related_name="questions"
)
def __str__(self):
return self.data[:50]

View File

@@ -6,7 +6,7 @@ from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.urls import reverse
from django.contrib import messages
from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite
from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite, QuestionImage, QuizImage
from .forms import QuizForm, QuestionForm
import json
from django.core.paginator import Paginator
@@ -193,10 +193,23 @@ def favorite_quiz(request, pk):
# Neues Quiz erstellen
@login_required
def new_quiz(request):
if request.method == 'POST':
form = QuizForm(request.POST, request.FILES)
if form.is_valid():
quiz = form.save(commit=False)
try:
quiz_image=request.FILES['quiz_image']
imagequiz=QuizImage.objects.create(image=quiz_image)
quiz.image=imagequiz
except:
pass
quiz.user_id = request.user
#quiz.creator = request.user
quiz.save()
@@ -215,7 +228,27 @@ def edit_quiz(request, pk):
form = QuizForm(request.POST, instance=quiz, files=request.FILES)
if form.is_valid():
if 'reset_image' in request.POST:
quiz.image = None
try:
quiz_image=request.FILES['quiz_image']
imagequiz=QuizImage.objects.create(image=quiz_image)
quiz.image=imagequiz
except:
pass
form.save()
for img in QuizImage.objects.all():
try:
img.delete()
img.image.delete(save=False)
except:
pass
return redirect('library:detail_quiz', pk=pk)
#return modified(request, pk)
@@ -229,6 +262,19 @@ def delete_quiz(request, pk):
quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
if request.method == 'POST':
quiz.delete()
for img in QuizImage.objects.all():
try:
img.delete()
img.image.delete(save=False)
except:
pass
for img in QuestionImage.objects.all():
try:
img.delete()
img.image.delete(save=False)
except:
pass
return redirect('library:overview_quiz')
return render(request, 'library/delete_confirmation.html', {'object': quiz})
@@ -256,6 +302,7 @@ def copy_quiz(request, pk):
original_quiz = get_object_or_404(QivipQuiz, pk=pk)
# Quiz kopieren (ohne ID, damit ein neues Objekt erstellt wird)
# Quiz kopieren (ohne ID, damit ein neues Objekt erstellt wird)
@@ -264,11 +311,10 @@ def copy_quiz(request, pk):
new_quiz.pk = None # Setzt die ID auf None, damit Django es als neues Objekt erkennt
new_quiz.uuid = uuid.uuid4() # Neue UUID generieren
new_quiz.user_id = request.user # Neuer Besitzer ist der aktuelle User
new_quiz.name += " - Kopie" # Optional: Name anpassen
new_quiz.name += " - Kopie"
new_quiz.base_quiz_id = pk
new_quiz.rating_count = 0
new_quiz.average_rating = 3.0
#new_quiz.creator = original_quiz.creator or request.user
new_quiz.save() # Speichern als neues Objekt
# Optional: Beschreibung anpassen
# Alle zugehörigen Fragen kopieren
@@ -278,6 +324,7 @@ def copy_quiz(request, pk):
question.pk = None # Setze pk auf None, damit es als neues Objekt gespeichert wird
question.quiz_id = original_quiz # Verknüpfe mit dem neuen Quiz
question.data = question.data # `data`-Feld wird explizit übernommen # Verknüpfe mit dem neuen Quiz
question.save()
messages.success(request, "Quiz wurde erfolgreich kopiert!")
@@ -318,7 +365,6 @@ def new_question(request):
if request.method == 'POST':
question_text = request.POST.get('question', '')
question_image = request.FILES.get('question_image', None)
# Handle different question types
if question_type == 'true_false':
@@ -358,7 +404,14 @@ def new_question(request):
question = QivipQuestion()
question.data = json.dumps(json_data)
question.quiz_id = quiz
question.image = question_image
try:
question_image=request.FILES['question_image']
imagequestion=QuestionImage.objects.create(image=question_image)
question.image=imagequestion
except:
pass
question.time_per_question = request.POST.get('time_per_question', '30')
question.save()
#return modified(request, pk=quiz.pk)
@@ -400,6 +453,8 @@ def new_question(request):
@login_required
def edit_question(request, pk):
question = get_object_or_404(QivipQuestion, pk=pk, quiz_id__user_id=request.user)
if 'reset_ques_image' in request.POST:
question.image = None
if question.quiz_id.user_id != request.user:
return redirect('library:question_list')
@@ -454,6 +509,8 @@ def edit_question(request, pk):
question_text = request.POST.get('question', '')
try:
question_image=request.FILES['question_image']
imagequestion=QuestionImage.objects.create(image=question_image)
question.image=imagequestion
except Exception:
pass
@@ -498,8 +555,15 @@ def edit_question(request, pk):
question.data = json.dumps(json_data)
question.time_per_question = request.POST.get('time_per_question', '30')
question.image = question_image
question.save()
for img in QuestionImage.objects.all():
try:
img.delete()
img.image.delete(save=False)
except:
pass
#return modified(request, question.quiz_id.pk)
return redirect('library:detail_quiz', pk=question.quiz_id.pk)
@@ -527,5 +591,11 @@ def delete_question(request, pk):
if request.method == 'POST':
question.delete()
for img in QuestionImage.objects.all():
try:
img.delete()
img.image.delete(save=False)
except:
pass
return redirect('library:detail_quiz', pk=question.quiz_id.pk)
return render(request, 'library/delete_confirmation.html', {'object': question})

View File

@@ -4,10 +4,8 @@ from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.utils import timezone
from django.urls import reverse
from django.db import models
from datetime import timedelta
from play.models import QuizGame, QuizGameParticipant, QuizAnswer
from django.conf import settings
class GameConsumer(AsyncWebsocketConsumer):
@@ -244,6 +242,8 @@ class GameConsumer(AsyncWebsocketConsumer):
@database_sync_to_async
def save_rating(self, participant_id, rating):
if not settings.QIVIP_CONFIG['ENABLE_RATING_SYSTEM']:
return False
from library.models import QuizRating
try:
game = QuizGame.objects.get(join_code=self.join_code)

View File

@@ -2,9 +2,10 @@ from django.utils import timezone
from django.shortcuts import render
from django.shortcuts import render, redirect, get_object_or_404, reverse
from play.models import QuizGame, QuizGameParticipant
from library.models import QivipQuiz, QivipQuestion
from library.models import QivipQuiz
from django.http import HttpResponseRedirect, HttpResponseNotFound
from django.contrib import messages
from django.conf import settings
import json
@@ -144,7 +145,8 @@ def finished(request, join_code):
context = {
'join_code': join_code,
'is_host': str(game.host_id) == str(request.session.get('host_id')),
'participant_id': participant_id if participant else None
'participant_id': participant_id if participant else None,
'rating_enabled': settings.QIVIP_CONFIG['ENABLE_RATING_SYSTEM']
}
return render(request, 'play/game/finished.html', context)
except QuizGame.DoesNotExist:

View File

@@ -7,6 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{% static 'css/t-style.css' %}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/@tsparticles/confetti@3.0.3/tsparticles.confetti.bundle.min.js"></script>
<title>qivip</title>
</head>
<body>

View File

@@ -193,24 +193,26 @@
<div class="flex justify-end">
{% if quiz.base_quiz %}
<p class="text-gray-600 mt-2 ">Bearbeitung:<span class="font-bold"> {{ quiz.user_id}}</span></p>
<p class="text-gray-600 mt-2 ">Bearbeitung:<span class="text-blue-600 font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.user_id }}">{{ quiz.user_id }}</a></span></p>
{% else %}
<p class="text-gray-600 mt-2 ">Autor:<span class="text-blue-600 font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.user_id }}">{{ quiz.user_id }}</a></span></p>
{% endif %}
</div>
<div class="flex justify-end">
{% if quiz.base_quiz %}
<p class="text-gray-600 ">Kopie von: <a href="{% url 'library:detail_quiz' quiz.base_quiz.id %}"><span class="font-bold">{{ quiz.base_quiz}}</span></a></p>
<p class="text-gray-600 ">Kopie von: <a href="{% url 'library:detail_quiz' quiz.base_quiz.id %}"><span class="text-blue-600 font-bold">{{ quiz.base_quiz}}</span></a></p>
{% endif %}
</div>
<div class="flex justify-end">
{% if quiz.base_quiz %}
<p class="text-gray-600 ">Autor:<span class="font-bold"> {{ quiz.base_quiz.user_id}}</span></p>
<p class="text-gray-600 ">Autor:<span class="text-blue-600 font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.base_quiz.user_id }}">{{ quiz.base_quiz.user_id }}</a></span></p>
{% endif %}
</div>
<div class="flex justify-end">
{% if quiz.credits %}
<p class="text-gray-600 mb-2">Credits:<span class="font-bold"> {{ quiz.credits }}</span></p>
<p class="text-gray-600 mb-2">Credits:<span class="text-blue-600 font-bold"> {{ quiz.credits }}</span></p>
{% endif %}
</div>

View File

@@ -5,6 +5,21 @@
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
{% if form.instance.image %}
<img src="{{ form.instance.image.image.url }}" style="max-width: 100px;" alt="Bild der Frage">
<label for="reset_image">
<input type="checkbox" name="reset_image" id="reset_image">
Bild zurücksetzen
</label>
{% endif %}
<div>
<label class="font-bold" for="quiz_image">Bild:</label>
<input class="bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400" type="file" name="quiz_image" id="quiz_image">
</div>
<div class="flex justify-center space-x-3">
<a href="{% if object.quiz_id %}{% url 'library:detail_quiz' object.quiz_id.id %}{% else %}{% url 'library:overview_quiz' %}{% endif %}"
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">

View File

@@ -55,15 +55,15 @@
<input type="hidden" name="filter" value="1">
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">minimale Anzahl an Fragen</label>
<input type="number" name="min_amout_questions" class="border-2 border-gray-300 rounded-lg p-2 w-full" min="1" id="id_min_amout_questions">
<input type="number" name="min_amout_questions" class="border-2 border-gray-300 rounded-lg p-2 w-full" min="1" id="id_min_amout_questions" value="{{ form.min_amout_questions.value|default_if_none:'' }}">
</div>
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">maximale Anzahl an Fragen</label>
<input type="number" name="max_amout_questions" class="border-2 border-gray-300 rounded-lg p-2 w-full" min="1" id="id_max_amout_questions">
<input type="number" name="max_amout_questions" class="border-2 border-gray-300 rounded-lg p-2 w-full" min="1" id="id_max_amout_questions" value="{{ form.max_amout_questions.value|default_if_none:'' }}">
</div>
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">veröffentlicht von User</label>
<input type="text" name="user" class="border-2 border-gray-300 rounded-lg p-2 w-full" id="id_user">
<input type="text" name="user" class="border-2 border-gray-300 rounded-lg p-2 w-full" id="id_user" value="{{ form.user.value|default_if_none:'' }}">
</div>
<div class="md:col-span-3 flex justify-end">
<button type="submit" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200">
@@ -97,7 +97,7 @@
<div class="relative w-full h-full inset-0 bg-cover bg-center">
<!-- Hintergrundbild -->
<div class="absolute inset-0 bg-cover bg-center"
style="background-image: url({{ quiz.image.url }});">
style="background-image: url({{ quiz.image.image.url }});">
</div>
</div>
@@ -167,6 +167,7 @@
<!-- Rating -->
<div class="flex items-center gap-1">
{% if quiz.rating_count > 0 %}
{% for i in '12345'|make_list %}
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
<span class="text-yellow-500 text-base"></span>
@@ -175,6 +176,9 @@
{% endif %}
{% endfor %}
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
{% else %}
<span class="text-gray-600 text-sm">Noch keine Bewertungen</span>
{% endif %}
</div>
</div>
@@ -284,7 +288,7 @@
<div class="relative w-full h-full inset-0 bg-cover bg-center">
<!-- Hintergrundbild -->
<div class="absolute inset-0 bg-cover bg-center"
style="background-image: url({{ quiz.image.url }});">
style="background-image: url({{ quiz.image.image.url }});">
</div>
</div>
@@ -355,6 +359,7 @@
<!-- Rating -->
<div class="flex items-center gap-1">
{% if quiz.rating_count > 0 %}
{% for i in '12345'|make_list %}
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
<span class="text-yellow-500 text-base"></span>
@@ -363,6 +368,9 @@
{% endif %}
{% endfor %}
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
{% else %}
<span class="text-gray-600 text-sm">Noch keine Bewertungen</span>
{% endif %}
</div>
</div>
@@ -479,7 +487,7 @@
<div class="relative w-full h-full inset-0 bg-cover bg-center">
<!-- Hintergrundbild -->
<div class="absolute inset-0 bg-cover bg-center"
style="background-image: url({{ quiz.image.url }});">
style="background-image: url({{ quiz.image.image.url }});">
</div>
</div>
@@ -548,6 +556,7 @@
<!-- Rating -->
<div class="flex items-center gap-1">
{% if quiz.rating_count > 0 %}
{% for i in '12345'|make_list %}
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
<span class="text-yellow-500 text-base"></span>
@@ -556,6 +565,9 @@
{% endif %}
{% endfor %}
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
{% else %}
<span class="text-gray-600 text-sm">Noch keine Bewertungen</span>
{% endif %}
</div>
</div>

View File

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

View File

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

View File

@@ -1,11 +1,15 @@
<nav class="flex justify-between items-center bg-blue-600 h-12 px-4 sm:px-6 rounded-lg mt-1 m-2 shadow-md overflow-x-auto whitespace-nowrap">
<nav id="main-nav" class="relative bg-blue-600 rounded-lg mt-1 m-2 shadow-md transition-all duration-300">
<!-- Desktop Navigation Bar -->
<div class="flex justify-between items-center h-12 px-4 sm:px-6 overflow-x-auto whitespace-nowrap">
<!-- Logo / Left section -->
<div class="flex items-center flex-shrink-0 md:w-32">
<h2 class="text-xl font-bold text-white hover:scale-110 transition duration-200">
<a href="{% url 'homepage:home' %}">qivip</a>
</h2>
</div>
<div class="flex justify-center items-center min-w-40">
<!-- Desktop Search (centered on screen) - Hidden on mobile -->
<div class="hidden md:flex justify-center items-center absolute left-1/2 transform -translate-x-1/2 min-w-40">
<form method="get" action="{% url 'library:overview_quiz' %}" class="mr-2 ml-2 flex items-center justify-center w-screen max-w-sm bg-white rounded-full px-4 py-1 shadow-md">
<input type="text" name="search" placeholder="Suche ..." value="{{ request.GET.search }}"
class="text-sm w-full px-3 py-1 text-black bg-transparent focus:outline-none lg:text-base">
@@ -14,13 +18,14 @@
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
</svg>
</button>
</form></div>
</form>
</div>
<div class="flex items-center space-x-4 md:w-40 ">
<!-- Desktop Navigation Links - Hidden on mobile -->
<div class="hidden md:flex items-center space-x-4 md:w-40">
<ul class="flex space-x-4 qp-nav-list">
{% if show_search %}
<li class="hidden md:flex"><a href="{% url 'library:overview_quiz' %}">Bibliothek</a></li>
<li><a href="{% url 'library:overview_quiz' %}">Bibliothek</a></li>
{% else%}
<li><a href="{% url 'library:overview_quiz' %}">Bibliothek</a></li>
{% endif %}
@@ -31,4 +36,86 @@
{% endif %}
</ul>
</div>
<!-- Mobile menu button - Only visible on mobile -->
<div class="flex md:hidden items-center">
<button id="mobile-menu-button" class="text-white focus:outline-none">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
<!-- Mobile Navigation Menu - Hidden by default - FULLWIDTH DROPDOWN -->
<div id="mobile-menu" class="hidden absolute w-full z-50 md:hidden opacity-0 transform -translate-y-2 transition-all duration-300">
<div class="bg-blue-500 pt-3 pb-4 rounded-b-lg shadow-inner">
<!-- Search in mobile menu -->
<form method="get" action="{% url 'library:overview_quiz' %}" class="px-4 mb-4">
<div class="flex items-center justify-center bg-white rounded-full px-4 py-2 shadow-md">
<input type="text" name="search" placeholder="Suche ..." value="{{ request.GET.search }}"
class="w-full text-sm px-2 text-black bg-transparent focus:outline-none">
<button class="text-blue-600">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-5">
<path stroke-linecap="round" stroke-linejoin="round"
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
</svg>
</button>
</div>
</form>
<!-- Mobile Navigation Links -->
<div class="space-y-2 px-4">
<a href="{% url 'library:overview_quiz' %}" class="block py-2 text-white font-medium rounded-lg hover:bg-blue-600 transition duration-200">
Bibliothek
</a>
{% if user.is_authenticated %}
<a href="{% url 'accounts:home' %}" class="block py-2 text-white font-medium rounded-lg hover:bg-blue-600 transition duration-200">
Konto
</a>
{% else %}
<a href="{% url 'accounts:login' %}" class="block py-2 text-white font-medium rounded-lg hover:bg-blue-600 transition duration-200">
Anmelden
</a>
{% endif %}
</div>
</div>
</div>
</nav>
<!-- Mobile Menu Script -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const mobileMenuButton = document.getElementById('mobile-menu-button');
const mobileMenu = document.getElementById('mobile-menu');
const mainNav = document.getElementById('main-nav');
mobileMenuButton.addEventListener('click', function() {
// Toggle menu visibility
if (mobileMenu.classList.contains('hidden')) {
// Show menu
mobileMenu.classList.remove('hidden');
// Entferne unten abgerundete Ecken der Navbar wenn Menü offen
mainNav.classList.remove('rounded-lg');
mainNav.classList.add('rounded-t-lg');
// Animation einfahren
setTimeout(() => {
mobileMenu.classList.remove('opacity-0', 'transform', '-translate-y-2');
mobileMenu.classList.add('opacity-100');
}, 50);
} else {
// Ausfahren Animation
mobileMenu.classList.add('opacity-0', 'transform', '-translate-y-2');
mobileMenu.classList.remove('opacity-100');
// Nach der Animation verstecken
setTimeout(() => {
mobileMenu.classList.add('hidden');
// Stelle die abgerundeten Ecken wieder her
mainNav.classList.remove('rounded-t-lg');
mainNav.classList.add('rounded-lg');
}, 300);
}
});
});
</script>

View File

@@ -11,6 +11,7 @@
</div>
{% if not is_host %}
{% if rating_enabled %}
<div class="bg-blue-50 rounded-lg p-6 mb-8">
<h2 class="text-2xl font-bold text-blue-800 mb-4 text-center">Wie hat dir das Quiz gefallen?</h2>
<div class="stars flex justify-center space-x-4 mb-4">
@@ -22,6 +23,9 @@
</div>
<p id="rating-message" class="text-center text-blue-600 font-medium"></p>
</div>
{% else %}
<p class="text-center text-blue-600 font-medium mb-8">Bewertungen sind deaktiviert</p>
{% endif %}
{% endif %}
<div class="text-center">

View File

@@ -15,7 +15,7 @@
{% if question_image %}
<div class="flex justify-center">
<img class="m-4" src="{{ question_image.url }}" style="max-width: 500px;" alt="Bild der Frage">
<img class="m-4" src="{{ question_image.image.url }}" style="max-width: 500px;" alt="Bild der Frage">
</div>
{% endif %}

View File

@@ -17,17 +17,25 @@
{% endfor %}
</div>
</div>
{% if request.session.mode != "learn" %}
{% if request.session.mode != "learn" %}
<div class="mb-6">
<h2 class="text-xl font-bold mb-4">Punktestand</h2>
<div class="space-y-2">
<div id="scoreboard" class="space-y-2">
{% for participant in participants %}
{% if participant == my_participant or is_host %}
<div class="p-4 {% if forloop.first %}bg-yellow-100 border-2 border-yellow-500{% else %}bg-gray-100{% endif %} rounded-lg flex justify-between items-center">
<div 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>
<p class="text-lg font-bold">{{ participant.display_name }}</p>
<p class="text-lg font-bold">
{% if forloop.counter == 1 %}
🥇
{% elif forloop.counter == 2 %}
🥈
{% elif forloop.counter == 3 %}
🥉
{% endif %}
{{ participant.display_name }}
</p>
<p class="text-gray-600">{{ participant.score }} Punkte</p>
</div>
{% if participant.last_answer_correct %}
@@ -36,13 +44,7 @@
<span class="text-red-500"></span>
{% endif %}
</div>
{% else %}
{% endif %}
{% endfor %}
</div>
</div>
@@ -63,6 +65,61 @@
{% endblock %}
{% 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)';
}
});
// Warten bevor Animation startet
await sleep(1500); // <<< hier die 5 Sekunden 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();
}
}, index * 1500); // 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' %}
<script>
const joinCode = '{{ quiz_game.join_code }}';

View File

@@ -5,3 +5,5 @@ daphne~=4.1.2 # ASGI server
pillow~=11.1.0 # Image handling
whitenoise~=6.6.0 # Static file serving
django-cleanup
redis
channels_redis