Compare commits

...

55 Commits

Author SHA1 Message Date
ben8
bd3c1555ae Abstand verändert 2025-11-16 16:46:01 +01:00
ben8
ccb8c4350d Merge branch '158-design-quiz-formular' into 'master'
Resolve "Design Quiz Formular"

Closes #158

See merge request enrichment-2024/qivip!115
2025-11-16 15:38:45 +00:00
ben8
f6382069d0 #158 Design fix 2025-11-16 16:38:15 +01:00
ben8
d65f5d8927 Merge branch '157-genauere-punktevergabe-bei-der-kartenfunktion' into 'master'
Resolve "Genauere Punktevergabe bei der Kartenfunktion!"

Closes #157

See merge request enrichment-2024/qivip!114
2025-11-15 17:54:52 +00:00
ben8
5897ec03c7 Genauigkeit bei Punkten für Karte 2025-11-15 18:54:12 +01:00
ben8
63d801d62b Hinweis zur Quiz-Statistik 2025-11-15 17:32:18 +01:00
ben8
f0f10593b4 Merge branch '156-fehler-bei-punktevergabe-bei-der-schatzfunktion' into 'master'
Resolve "Fehler bei Punktevergabe bei der Schätzfunktion"

Closes #156

See merge request enrichment-2024/qivip!113
2025-11-15 16:22:10 +00:00
ben8
147f702a39 #156 fix 2025-11-15 17:21:31 +01:00
ben8
53c7199895 Merge branch '155-quiz-design-anpassen' into 'master'
Resolve "Quiz Design anpassen!"

Closes #155

See merge request enrichment-2024/qivip!112
2025-11-15 16:03:15 +00:00
ben8
0371fc6ade #155 design for credits 2025-11-15 17:00:17 +01:00
ben8
1bbafefa71 Merge branch '154-anzahl-wie-oft-ein-quiz-gespielt-worden-ist' into 'master'
Resolve "Anzahl, wie oft ein Quiz gespielt worden ist."

Closes #154

See merge request enrichment-2024/qivip!111
2025-11-14 16:25:26 +00:00
ben8
304cf0a01b #154-counter_played_quizzes 2025-11-14 17:24:51 +01:00
ben8
92214d3bda Merge branch '85-fragentyp-ort-suchen' into 'master'
Resolve "Fragentyp: Ort suchen"

Closes #85

See merge request enrichment-2024/qivip!103
2025-11-01 18:41:37 +00:00
ben8
ce0b8305b4 geopy and geographiclib zu Paketen hinzugefügt 2025-11-01 19:40:48 +01:00
ben8
ee823629b9 Teilnehmer_Koordinaten-einzeichnen 2025-11-01 17:09:42 +01:00
ben8
57d325df17 small_fix 2025-11-01 15:43:35 +01:00
ben8
4f320d5c8b Fix 2025-10-31 20:26:33 +01:00
ben8
bd3c956e19 style 2025-10-27 12:37:47 +01:00
ben8
7e892571fb Design-Anpassung (Formular) 2025-10-24 19:52:58 +02:00
ben8
63107a00e6 Merge branch '85-fragentyp-ort-suchen' of edugit.org:enrichment-2024/qivip into 85-fragentyp-ort-suchen 2025-10-24 18:47:46 +02:00
juhn
0d098c38eb adjust map score tolerance 2025-10-24 18:24:59 +02:00
juhnsa
fa544d8228 Fix errors 2025-10-24 18:24:59 +02:00
juhnsa
e816c3f75a First Iteration of Map Question Type 2025-10-24 18:24:59 +02:00
ben8
bb36b3e71f Merge branch '153-footer-fix' into 'master'
Resolve "Footer fix"

Closes #153

See merge request enrichment-2024/qivip!110
2025-10-24 16:17:42 +00:00
ben8
27a5a78e10 fix footer 2025-10-24 18:17:11 +02:00
juhn
657b81cd46 adjust map score tolerance 2025-10-24 16:03:16 +02:00
juhnsa
07b9ddf745 Fix errors 2025-10-24 16:03:16 +02:00
juhnsa
ded3328d06 First Iteration of Map Question Type 2025-10-24 16:03:06 +02:00
ben8
760ba5be06 Paket_Liste überarbeitet 2025-10-21 18:14:42 +02:00
ben8
46052d136b Merge branch '64-footer-muss-unten-angezeigt-werden-2' into 'master'
Resolve "Footer muss unten angezeigt werden"

Closes #64

See merge request enrichment-2024/qivip!109
2025-10-21 15:48:32 +00:00
ben8
3701442e8b Footer überarbeitet 2025-10-21 17:37:13 +02:00
ben8
c21363e2ac Merge branch '152-css' into 'master'
Resolve "css"

Closes #152

See merge request enrichment-2024/qivip!108
2025-10-09 17:27:08 +00:00
ben8
35d5884156 design-fix 2025-10-09 18:13:51 +02:00
nik8
a5337b1fde Merge branch '151-der-text-ist-teilweise-nicht-mehr-zu-sehen-im-erstellmodus' into 'master'
Resolve "Der Text ist teilweise nicht mehr zu sehen!!! Im Erstellmodus!"

Closes #151

See merge request enrichment-2024/qivip!107
2025-09-27 17:07:13 +00:00
nik8
3b37bd8338 Fix_Schrift + request bei requirements 2025-09-27 19:06:44 +02:00
ben8
df4f7e92bc Merge branch '141-richtig-schones-cooles-siegerpodest-2' into 'master'
Resolve "Richtig schönes, cooles Siegerpodest."

Closes #141

See merge request enrichment-2024/qivip!106
2025-07-29 17:02:51 +00:00
ben8
d03c99b8e3 schönes Siegerpodest (und bessere Punkteübersicht) 2025-07-29 19:01:20 +02:00
ben8
c9ddecfba7 Punktevergabe fix Fragetyp:order 2025-07-24 21:50:18 +02:00
ben8
2da75fdb52 podest_1.Version 2025-07-24 21:02:41 +02:00
ben8
86a4ecc351 Merge branch '148-schatzen-fragentyp' into 'master'
Resolve "Schätzen - Fragentyp"

Closes #148

See merge request enrichment-2024/qivip!105
2025-07-21 17:08:59 +00:00
ben8
07633dcaf9 Schätzfunktion( (mit Schieberegler) #148 2025-07-21 18:56:17 +02:00
ben8
885eb1e528 Merge branch '147-quiztyp-reihenfolge' into 'master'
Resolve "Quiztyp - Reihenfolge"

Closes #147

See merge request enrichment-2024/qivip!104
2025-07-20 12:07:18 +00:00
ben8
9397da4cfd Reihenfolge_Fragetyp(und Lizenzenz_Dateien) 2025-07-20 14:06:52 +02:00
ben8
f778107c30 Reihenfolge-Fragetyp (und Lizenzenübersicht) 2025-07-20 14:05:51 +02:00
ben8
826af8beb2 Merge branch '146-pdf-erstellen-fur-quiz' into 'master'
Resolve "PDF - erstellen für Quiz"

Closes #146

See merge request enrichment-2024/qivip!102
2025-07-13 11:32:02 +00:00
ben8
f80e35a0d0 fix-Multiple-choice (verursacht bei Änderung - darkMode) 2025-07-13 13:31:25 +02:00
ben8
736db1c480 PDF 2025-07-13 13:12:45 +02:00
ben8
4ac5dd794b Merge branch '145-dark-mode' into 'master'
Resolve "Dark mode"

Closes #145

See merge request enrichment-2024/qivip!101
2025-07-11 14:57:56 +00:00
ben8
d17d41e07a dark_mode 2025-07-11 16:54:00 +02:00
ben8
7f845b8f36 Merge branch '24-rahmen-um-die-quiz-in-der-bibliothek-farbe-and-die-kategorie' into 'master'
Resolve "Rahmen um die Quiz in der Bibliothek Farbe and die Kategorie."

Closes #24

See merge request enrichment-2024/qivip!100
2025-07-04 11:57:25 +00:00
ben8
37f4a7b868 Kategorie_mit_Farbpunkt 2025-07-04 13:56:32 +02:00
ben8
d929e6b5dd weiter Button in überspringen umbenannt 2025-06-29 17:45:42 +02:00
ben8
6e63e0876a optisches Update - Schrift 2025-06-29 17:43:28 +02:00
ben8
d907390652 credits_verbessert 2025-06-29 17:11:04 +02:00
ben8
31e9618e1c credits_darstellung_verbessert 2025-06-29 17:10:25 +02:00
67 changed files with 4809 additions and 537 deletions

View File

@@ -170,3 +170,12 @@ Die Konfiguration des Projekts befindet sich im `config/qivip_config.json`.
| Variable | Bedeutung | Optionen | | Variable | Bedeutung | Optionen |
| --- | --- | --- | | --- | --- | --- |
| ENABLE_RATING_SYSTEM | Soll das Bewertungssystem aktiviert werden? | true [Standard], false | | ENABLE_RATING_SYSTEM | Soll das Bewertungssystem aktiviert werden? | true [Standard], false |
## Lizenzen für node_modules
1. Installation: npm install license-checker --save-dev
npx license-checker --markdown > THIRD_PARTY_LICENSES.md
## für Python Packete:
pip-licenses --format=markdown > THIRD_PARTY_LICENSES_PYTHON.md

Binary file not shown.

View File

@@ -0,0 +1,44 @@
| Name | Version | License |
|--------------------|--------------|---------------------------------------------------------|
| Automat | 25.4.16 | MIT License |
| Django | 5.1.13 | BSD License |
| Twisted | 25.5.0 | MIT License |
| asgiref | 3.10.0 | BSD License |
| attrs | 25.4.0 | UNKNOWN |
| autobahn | 25.10.1 | UNKNOWN |
| certifi | 2025.10.5 | Mozilla Public License 2.0 (MPL 2.0) |
| cffi | 2.0.0 | UNKNOWN |
| channels | 4.2.2 | BSD License |
| channels_redis | 4.3.0 | BSD |
| chardet | 5.2.0 | GNU Lesser General Public License v2 or later (LGPLv2+) |
| charset-normalizer | 3.4.4 | MIT |
| colorama | 0.4.6 | BSD License |
| constantly | 23.10.4 | MIT License |
| cryptography | 46.0.3 | UNKNOWN |
| daphne | 4.1.2 | BSD License |
| django-cleanup | 9.0.0 | MIT License |
| geographiclib | 2.1 | MIT |
| geopy | 2.4.1 | MIT License |
| hyperlink | 21.0.0 | MIT License |
| idna | 3.11 | UNKNOWN |
| incremental | 24.7.2 | MIT License |
| msgpack | 1.1.2 | UNKNOWN |
| pillow | 11.1.0 | CMU License (MIT-CMU) |
| pyOpenSSL | 25.3.0 | Apache Software License |
| pyasn1 | 0.6.1 | BSD License |
| pyasn1_modules | 0.4.2 | BSD License |
| pycparser | 2.23 | BSD License |
| pypng | 0.20220715.0 | MIT License |
| python-dotenv | 1.1.1 | BSD License |
| qrcode | 7.4.2 | BSD License |
| redis | 6.4.0 | MIT License |
| reportlab | 4.2.5 | BSD License |
| requests | 2.32.5 | Apache Software License |
| service-identity | 24.2.0 | MIT License |
| sqlparse | 0.5.3 | BSD License |
| txaio | 25.9.2 | UNKNOWN |
| typing_extensions | 4.15.0 | UNKNOWN |
| tzdata | 2025.2 | Apache Software License |
| urllib3 | 2.5.0 | UNKNOWN |
| whitenoise | 6.6.0 | MIT License |
| zope.interface | 8.0.1 | Zope Public License |

View File

@@ -30,10 +30,35 @@ class QuizImageAdmin(admin.ModelAdmin):
class QuestionImageAdmin(admin.ModelAdmin): class QuestionImageAdmin(admin.ModelAdmin):
list_display = ('image',) list_display = ('image',)
from django import forms
from .models import QuizCategory
class QuizCategoryForm(forms.ModelForm):
color = forms.CharField(
widget=forms.TextInput(attrs={'type': 'color'})
)
class Meta:
model = QuizCategory
fields = '__all__'
from django.utils.html import format_html
@admin.register(QuizCategory)
class QuizCategoryAdmin(admin.ModelAdmin):
form = QuizCategoryForm
list_display = ('name', 'color_preview', 'color')
def color_preview(self, obj):
return format_html(
'<div style="width: 24px; height: 24px; background-color: {}; border: 2px solid #000;"></div>',
obj.color
)
color_preview.short_description = "Farbe"
# Registrierung der Modelle im Admin # Registrierung der Modelle im Admin
admin.site.register(QivipQuiz, QivipQuizAdmin) admin.site.register(QivipQuiz, QivipQuizAdmin)
admin.site.register(QivipQuestion, QivipQuestionAdmin) admin.site.register(QivipQuestion, QivipQuestionAdmin)
admin.site.register(QuizCategory, QuizCategoryAdmin) #admin.site.register(QuizCategory, QuizCategoryAdmin)
admin.site.register(QivipQuizFavorite, QivipQuizFavoriteAdmin) admin.site.register(QivipQuizFavorite, QivipQuizFavoriteAdmin)
admin.site.register(QuizImage, QuizImageAdmin) admin.site.register(QuizImage, QuizImageAdmin)
admin.site.register(QuestionImage, QuestionImageAdmin) admin.site.register(QuestionImage, QuestionImageAdmin)

View File

@@ -6,6 +6,15 @@ class QuizForm(forms.ModelForm):
model = QivipQuiz model = QivipQuiz
fields = ['name', 'description', 'status', 'category','difficulty','credits'] fields = ['name', 'description', 'status', 'category','difficulty','credits']
def __init__(self, *args, **kwargs):
super(QuizForm, self).__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs.update({
'class': 'dark:bg-[#2a2f3a]! dark:text-white!'
})
class QuestionForm(forms.ModelForm): class QuestionForm(forms.ModelForm):
class Meta: class Meta:
model = QivipQuestion model = QivipQuestion
@@ -34,5 +43,5 @@ class QuizFilterForm(forms.Form):
required=False, required=False,
label="Kategorie", label="Kategorie",
widget=forms.Select(attrs={ widget=forms.Select(attrs={
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full' 'class': 'border-2 border-gray-300 rounded-lg p-2 w-full dark:bg-[#2a2f3a] dark:text-white'
})) }))

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-07-02 11:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0056_alter_quizrating_unique_together'),
]
operations = [
migrations.AddField(
model_name='quizcategory',
name='color',
field=models.CharField(default='#000000', help_text='Farbe als Hex-Code, z.B. #ff0000', max_length=7),
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.1.7 on 2025-11-14 14:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0057_quizcategory_color'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='quiz_played_Lernmodus',
field=models.IntegerField(blank=True, default=0, null=True),
),
migrations.AddField(
model_name='qivipquiz',
name='quiz_played_Moderiermodus',
field=models.IntegerField(blank=True, default=0, null=True),
),
migrations.AddField(
model_name='qivipquiz',
name='quiz_played_Moderiermodus_players',
field=models.IntegerField(blank=True, default=0, null=True),
),
migrations.AddField(
model_name='qivipquiz',
name='single_player_or_multiple',
field=models.CharField(blank=True, max_length=20),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.1.7 on 2025-11-14 15:04
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0058_qivipquiz_quiz_played_lernmodus_and_more'),
]
operations = [
migrations.RemoveField(
model_name='qivipquiz',
name='single_player_or_multiple',
),
]

View File

@@ -45,6 +45,10 @@ class QivipQuiz(models.Model):
) )
quiz_played_Lernmodus = models.IntegerField(null=True, blank=True, default=0)
quiz_played_Moderiermodus= models.IntegerField(null=True, blank=True, default=0)
quiz_played_Moderiermodus_players= models.IntegerField(null=True, blank=True, default=0)
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
user_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='quiz') 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') #creator = models.ForeignKey(User, on_delete=models.SET_NULL,blank=True, null=True, related_name='creator_quiz')
@@ -148,6 +152,12 @@ class QuizCategory(models.Model):
name = models.CharField(max_length=100, unique=True) name = models.CharField(max_length=100, unique=True)
description = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True)
slug = models.SlugField(unique=True, blank=True) slug = models.SlugField(unique=True, blank=True)
color= models.CharField(
max_length=7,
default="#000000",
help_text="Farbe als Hex-Code, z.B. #ff0000"
)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.slug: if not self.slug:

View File

@@ -20,5 +20,7 @@ urlpatterns = [
path('detail/copy/<int:pk>/', views.copy_quiz, name='copy_quiz'), path('detail/copy/<int:pk>/', views.copy_quiz, name='copy_quiz'),
path('favorite_quiz/<int:pk>/', views.favorite_quiz, name='favorite_quiz'), path('favorite_quiz/<int:pk>/', views.favorite_quiz, name='favorite_quiz'),
path("quiz-names-json/", views.quiz_names_json, name="quiz_names_json"), path("quiz-names-json/", views.quiz_names_json, name="quiz_names_json"),
#path('quiz/<int:pk>/pdf/', views.pdf_solution, name='quiz_solution'),
#path('quiz/<int:pk>/pdf/task', views.pdf_task, name='quiz_task'),
] ]

View File

@@ -17,6 +17,329 @@ from django.contrib.auth.models import User
from urllib.parse import urlparse, urlunparse from urllib.parse import urlparse, urlunparse
# Übersicht aller Quizze # Übersicht aller Quizze
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
from reportlab.lib import colors
from django.http import HttpResponse
import json
from datetime import datetime
from reportlab.platypus import Image
from reportlab.lib import colors
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.pagesizes import A4
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
import json
from io import BytesIO
import requests
from django.core.files.storage import default_storage
from PIL import Image as PILImage, ImageOps
from django.conf import settings
import os
from django.utils.html import escape
"""
def pdf_solution(request, pk):
quiz = get_object_or_404(QivipQuiz, pk=pk)
questions = QivipQuestion.objects.filter(quiz_id=quiz)
for question in questions:
if question.data:
question.data = json.loads(question.data)
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="Lösungen - {quiz.name}.pdf"'
doc = SimpleDocTemplate(response, pagesize=A4,
rightMargin=40, leftMargin=40,
topMargin=60, bottomMargin=60)
styles = getSampleStyleSheet()
# Stil für Quiz-Titel
title_style = ParagraphStyle(
'TitleStyle',
parent=styles['Title'],
alignment=1, # zentriert
fontSize=14,
spaceAfter=2,
)
# Stil für Fragen-Box
question_box_style = ParagraphStyle(
'QuestionBox',
fontSize=10,
leading=16,
fontName='Helvetica-Bold',
spaceAfter=8,
leftIndent=6,
rightIndent=6,
alignment=0
)
# Stil für Antwort (richtig)
answer_correct_style = ParagraphStyle(
'AnswerCorrect',
fontSize=11,
leading=14,
textColor=colors.green,
leftIndent=20,
spaceAfter=4,
)
# Stil für Antwort (falsch)
answer_wrong_style = ParagraphStyle(
'AnswerWrong',
fontSize=11,
leading=14,
textColor=colors.red,
leftIndent=20,
spaceAfter=4,
)
# Stil für normalen Antwort-Text (ohne Antwortanzeige)
answer_neutral_style = ParagraphStyle(
'AnswerNeutral',
fontSize=11,
leading=14,
textColor=colors.black,
leftIndent=20,
spaceAfter=4,
)
date_style = ParagraphStyle(
name='RightAligned',
parent=styles['Normal'],
alignment=2,
)
story = []
story.append(Paragraph(f"Quiz: {quiz.name}", title_style))
story.append(Paragraph(f"- Lösungen -", title_style))
story.append(Spacer(1, 20))
story.append(Paragraph(datetime.today().strftime("%d.%m.%Y"), date_style))
story.append(Spacer(1, 20))
if quiz.description:
story.append(Paragraph(quiz.description, styles['Normal']))
story.append(Spacer(1, 20))
for i, question in enumerate(questions, start=1):
data = question.data
frage_text = data.get('question', 'Frage nicht gefunden.')
story.append(Paragraph(f"Frage {i}: {frage_text}", question_box_style))
story.append(Spacer(1, 6))
rl_image = None
if hasattr(question, 'image') and question.image and question.image.image:
image_url = request.build_absolute_uri(question.image.image.url)
img_response = requests.get(image_url)
if img_response.status_code == 200:
image_data = BytesIO(img_response.content)
pil_image = PILImage.open(image_data)
pil_image = ImageOps.exif_transpose(pil_image)
orig_width, orig_height = pil_image.size
max_width = 80
max_height = 80
ratio = min(max_width / orig_width, max_height / orig_height)
new_width = orig_width * ratio
new_height = orig_height * ratio
output = BytesIO()
pil_image.save(output, format='JPEG')
output.seek(0)
rl_image = Image(output, width=new_width, height=new_height)
options = data.get('options', [])
answer_paragraphs = []
for idx, option in enumerate(options, start=1):
opt_text = option.get('value', '')
if option.get('is_correct', False):
text = f"{opt_text}"
style = answer_correct_style
else:
text = f"{opt_text}"
style = answer_wrong_style
answer_paragraphs.append(Paragraph(text, style))
if rl_image:
# Tabelle mit Bild links und Antworten rechts
from reportlab.platypus import Table, TableStyle
from reportlab.lib.units import cm
data_table = [[rl_image, answer_paragraphs]]
table = Table(data_table, colWidths=[6*cm, 10*cm])
table.setStyle(TableStyle([
('VALIGN', (0,0), (-1,-1), 'TOP'),
('LEFTPADDING', (0,0), (-1,-1), 0),
('RIGHTPADDING', (0,0), (-1,-1), 10),
]))
story.append(table)
else:
# Kein Bild, Antworten einfach nacheinander
for p in answer_paragraphs:
story.append(p)
story.append(Spacer(1, 12))
story.append(HRFlowable(width="100%", thickness=1, color=colors.grey))
story.append(Spacer(1, 12))
current_url = request.build_absolute_uri(f"/library/detail/{quiz.pk}/")
escaped_url = escape(current_url) # Sicherheitshalber escapen
link_html = f'<a href="{escaped_url}" color="blue">{escaped_url}</a>'
story.append(Paragraph(link_html, styles['Normal']))
doc.build(story)
return response
def pdf_task(request, pk):
quiz = get_object_or_404(QivipQuiz, pk=pk)
questions = QivipQuestion.objects.filter(quiz_id=quiz)
for question in questions:
if question.data:
question.data = json.loads(question.data)
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="Aufgabenblatt - {quiz.name}.pdf"'
doc = SimpleDocTemplate(response, pagesize=A4,
rightMargin=40, leftMargin=40,
topMargin=60, bottomMargin=60)
styles = getSampleStyleSheet()
title_style = ParagraphStyle(
'TitleStyle',
parent=styles['Title'],
alignment=1,
fontSize=14,
spaceAfter=2,
fontName='Helvetica-Bold',
)
question_box_style = ParagraphStyle(
'QuestionBox',
fontSize=10,
leading=16,
fontName='Helvetica-Bold',
spaceAfter=8,
leftIndent=6,
rightIndent=6,
alignment=0
)
answer_neutral_style = ParagraphStyle(
'AnswerNeutral',
fontSize=11,
leading=14,
textColor=colors.black,
leftIndent=20,
spaceAfter=4,
)
date_style = ParagraphStyle(
name='RightAligned',
parent=styles['Normal'],
alignment=2,
)
name_style = ParagraphStyle(
name='RightAligned',
parent=styles['Normal'],
alignment=0,
)
story = []
# Titel & Datum
story.append(Paragraph(f"Aufgabenblatt: {quiz.name}", title_style))
story.append(Spacer(1, 20))
story.append(Paragraph(datetime.today().strftime("%d.%m.%Y"), date_style))
story.append(Spacer(1, 20))
story.append(Paragraph("Name: ", name_style))
story.append(Spacer(1, 20))
if quiz.description:
story.append(Paragraph(quiz.description, styles['Normal']))
story.append(Spacer(1, 20))
for i, question in enumerate(questions, start=1):
data = question.data
frage_text = data.get('question', 'Frage nicht gefunden.')
story.append(Paragraph(f"Frage {i}: {frage_text}", question_box_style))
story.append(Spacer(1, 6))
rl_image = None
if hasattr(question, 'image') and question.image and question.image.image:
image_url = request.build_absolute_uri(question.image.image.url)
img_response = requests.get(image_url)
if img_response.status_code == 200:
image_data = BytesIO(img_response.content)
pil_image = PILImage.open(image_data)
pil_image = ImageOps.exif_transpose(pil_image)
orig_width, orig_height = pil_image.size
max_width = 80 # gleich wie bei Lösungen
max_height = 80
ratio = min(max_width / orig_width, max_height / orig_height)
new_width = orig_width * ratio
new_height = orig_height * ratio
output = BytesIO()
pil_image.save(output, format='JPEG')
output.seek(0)
rl_image = Image(output, width=new_width, height=new_height)
options = data.get('options', [])
answer_paragraphs = []
for idx, option in enumerate(options, start=1):
opt_text = option.get('value', '')
if data.get("type") !="input":
opt_text = option.get('value', '')
answer_paragraphs.append(Paragraph(f"{idx}. {opt_text}", answer_neutral_style))
else:
story.append(Paragraph("Deine Antwort: ", answer_neutral_style))
story.append(Spacer(1, 12))
if rl_image:
from reportlab.platypus import Table, TableStyle
from reportlab.lib.units import cm
data_table = [[rl_image, answer_paragraphs]]
table = Table(data_table, colWidths=[6*cm, 10*cm])
table.setStyle(TableStyle([
('VALIGN', (0,0), (-1,-1), 'TOP'),
('LEFTPADDING', (0,0), (-1,-1), 0),
('RIGHTPADDING', (0,0), (-1,-1), 10),
]))
story.append(table)
else:
for p in answer_paragraphs:
story.append(p)
story.append(Spacer(1, 12))
story.append(HRFlowable(width="100%", thickness=1, color=colors.grey))
story.append(Spacer(1, 12))
current_url = request.build_absolute_uri(f"/library/detail/{quiz.pk}/")
escaped_url = escape(current_url) # Sicherheitshalber escapen
link_html = f'<a href="{escaped_url}" color="blue">{escaped_url}</a>'
story.append(Paragraph(link_html, styles['Normal']))
doc.build(story)
return response
"""
def quiz_names_json(request): def quiz_names_json(request):
form = QuizFilterForm(request.GET) form = QuizFilterForm(request.GET)
@@ -488,6 +811,9 @@ def copy_quiz(request, pk):
new_quiz.base_quiz_id = pk new_quiz.base_quiz_id = pk
new_quiz.rating_count = 0 new_quiz.rating_count = 0
new_quiz.average_rating = 3.0 new_quiz.average_rating = 3.0
new_quiz.quiz_played_Lernmodus = 0
new_quiz.quiz_played_Moderiermodus = 0
new_quiz.quiz_played_Moderiermodus_players = 0
if original_quiz.status=="öffentlich": if original_quiz.status=="öffentlich":
new_quiz.published_at = timezone.now() new_quiz.published_at = timezone.now()
else: else:
@@ -526,6 +852,7 @@ def question_list(request):
def new_question(request): def new_question(request):
question_type = request.GET.get('type') question_type = request.GET.get('type')
quiz_id = request.GET.get('quiz_id') quiz_id = request.GET.get('quiz_id')
question_data = None
if not quiz_id: if not quiz_id:
messages.error(request, 'Quiz ID muss angegeben werden.') messages.error(request, 'Quiz ID muss angegeben werden.')
@@ -537,12 +864,12 @@ def new_question(request):
messages.error(request, 'Quiz nicht gefunden oder keine Berechtigung.') messages.error(request, 'Quiz nicht gefunden oder keine Berechtigung.')
return redirect('library:overview_quiz') return redirect('library:overview_quiz')
if question_type not in ['multiple_choice', 'true_false','input']: if question_type not in ['multiple_choice', 'true_false','input','order','guess','map']:
base_url = reverse('library:new_question') base_url = reverse('library:new_question')
return redirect(f'{base_url}?type=multiple_choice&quiz_id={quiz_id}') return redirect(f'{base_url}?type=multiple_choice&quiz_id={quiz_id}')
if request.method == 'POST': if request.method == 'POST':
question_text = request.POST.get('question', '') question_text = request.POST.get('question', 'Kein Fragentext angegeben')
# Handle different question types # Handle different question types
if question_type == 'true_false': if question_type == 'true_false':
@@ -566,7 +893,7 @@ def new_question(request):
] ]
} }
elif question_type == 'multiple_choice': elif question_type == 'multiple_choice' :
options = [] options = []
i = 1 i = 1
# Limit to maximum 6 options # Limit to maximum 6 options
@@ -590,6 +917,59 @@ def new_question(request):
'options': options 'options': options
} }
elif question_type == 'order' :
options = []
i = 1
# Limit to maximum 6 options
while request.POST.get(f'option_{i}') and i <= 6:
is_correct = request.POST.get(f'correct_{i}') == '1'
options.append({
'order': i,
'value': request.POST.get(f'option_{i}'),
})
i += 1
# Validate minimum 2 options
if len(options) < 2:
messages.error(request, 'Mindestens zwei Optionen müssen angegeben werden.')
return redirect(request.get_full_path())
json_data = {
'type': question_type,
'question': question_text,
'options': options
}
elif question_type == 'guess':
correct_answer = request.POST.get('correct_answer')
min_value = request.POST.get('min')
max_value = request.POST.get('max')
tolerance = request.POST.get('tolerance')
json_data = {
'type': question_type,
'question': question_text,
'correct_answer': correct_answer,
'min': min_value,
'max': max_value,
'tolerance': tolerance,
'options': [
{'value': correct_answer, 'is_correct': True}
]
}
elif question_type == 'map':
target_location = request.POST.get('location')
tolerance_radius = request.POST.get('tolerance_radius')
json_data = {
'type': question_type,
'question': question_text,
'target_location': target_location,
'tolerance_radius': tolerance_radius
}
question = QivipQuestion() question = QivipQuestion()
question.data = json.dumps(json_data) question.data = json.dumps(json_data)
question.quiz_id = quiz question.quiz_id = quiz
@@ -644,6 +1024,43 @@ def new_question(request):
} }
standard_time_per_question = 30 standard_time_per_question = 30
elif question_type == 'order':
question_data = {
'type': question_type,
'question': '',
'options': [
{'value': ''},
{'value': ''}
]
}
standard_time_per_question = 30
elif question_type == 'guess':
correct_answer = request.POST.get('correct_answer')
min_value = request.POST.get('min')
max_value = request.POST.get('max')
tolerance = request.POST.get('tolerance')
json_data = {
'type': question_type,
'question': '',
'correct_answer': correct_answer,
'min': min_value,
'max': max_value,
'tolerance': tolerance,
'options': [
{'value': correct_answer, 'is_correct': True}
]
}
standard_time_per_question = 30
elif question_type == 'map':
question_data = {
'type': question_type,
'question': '',
'target_location': "",
'tolerance_radius': 90000
}
standard_time_per_question = 30
template_name = f'library/question/question_{question_type}.html' template_name = f'library/question/question_{question_type}.html'
context = { context = {
'question': question_data, 'question': question_data,
@@ -684,6 +1101,13 @@ def edit_question(request, pk):
{'value': 'Falsch', 'is_correct': not correct_answer} {'value': 'Falsch', 'is_correct': not correct_answer}
] ]
} }
elif question_type == 'map':
question_data = {
'type': question_type,
'question': question_data.get('question', ''),
'target_location': question_data.get('target_location', ''),
'tolerance_radius': question_data.get('tolerance_radius', 90000)
}
else: else:
# For multiple choice questions # For multiple choice questions
question_data.setdefault('type', question_type) question_data.setdefault('type', question_type)
@@ -769,6 +1193,59 @@ def edit_question(request, pk):
'options': options 'options': options
} }
elif question_type == 'order':
options = []
i = 1
# Limit to maximum 6 options
while request.POST.get(f'option_{i}') and i <= 6:
options.append({
'order': i,
'value': request.POST.get(f'option_{i}')
})
i += 1
# Validate minimum 2 options
if len(options) < 2:
messages.error(request, 'Mindestens zwei Optionen müssen angegeben werden.')
return redirect(request.get_full_path())
# Validate maximum 6 options
if len(options) > 6:
messages.error(request, 'Maximal 6 Optionen sind erlaubt.')
return redirect(request.get_full_path())
json_data = {
'type': question_type,
'question': question_text,
'options': options
}
elif question_type == 'guess':
correct_answer = request.POST.get('correct_answer')
min_value = request.POST.get('min')
max_value = request.POST.get('max')
tolerance = request.POST.get('tolerance')
json_data = {
'type': question_type,
'question': question_text,
'correct_answer': correct_answer,
'min': min_value,
'max': max_value,
'tolerance': tolerance,
'options': [
{'value': correct_answer, 'is_correct': True}
]
}
elif question_type == 'map':
target_location = request.POST.get('location')
tolerance_radius = request.POST.get('tolerance_radius')
json_data = {
'type': question_type,
'question': question_text,
'target_location': target_location,
'tolerance_radius': tolerance_radius
}
question.data = json.dumps(json_data) question.data = json.dumps(json_data)
question.time_per_question = request.POST.get('time_per_question', '30') question.time_per_question = request.POST.get('time_per_question', '30')
try: try:

573
django/package-lock.json generated
View File

@@ -10,7 +10,11 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@tailwindcss/cli": "^4.1.5", "@tailwindcss/cli": "^4.1.5",
"sortablejs": "^1.15.6",
"tailwindcss": "^4.1.5" "tailwindcss": "^4.1.5"
},
"devDependencies": {
"license-checker": "^25.0.1"
} }
}, },
"node_modules/@parcel/watcher": { "node_modules/@parcel/watcher": {
@@ -537,6 +541,55 @@
"node": ">= 10" "node": ">= 10"
} }
}, },
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"dev": true
},
"node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/array-find-index": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
"integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"dev": true
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/braces": { "node_modules/braces": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
@@ -548,6 +601,60 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
"node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/debuglog": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz",
"integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"dev": true,
"engines": {
"node": "*"
}
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
@@ -559,6 +666,16 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/dezalgo": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
"integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
"dev": true,
"dependencies": {
"asap": "^2.0.0",
"wrappy": "1"
}
},
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.18.1", "version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
@@ -571,6 +688,15 @@
"node": ">=10.13.0" "node": ">=10.13.0"
} }
}, },
"node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -582,11 +708,106 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
}, },
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -622,6 +843,33 @@
"jiti": "lib/jiti-cli.mjs" "jiti": "lib/jiti-cli.mjs"
} }
}, },
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true
},
"node_modules/license-checker": {
"version": "25.0.1",
"resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz",
"integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==",
"dev": true,
"dependencies": {
"chalk": "^2.4.1",
"debug": "^3.1.0",
"mkdirp": "^0.5.1",
"nopt": "^4.0.1",
"read-installed": "~4.0.3",
"semver": "^5.5.0",
"spdx-correct": "^3.0.0",
"spdx-expression-parse": "^3.0.0",
"spdx-satisfies": "^4.0.0",
"treeify": "^1.1.0"
},
"bin": {
"license-checker": "bin/license-checker"
}
},
"node_modules/lightningcss": { "node_modules/lightningcss": {
"version": "1.29.2", "version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
@@ -859,6 +1107,39 @@
"node": ">=8.6" "node": ">=8.6"
} }
}, },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/mri": { "node_modules/mri": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@@ -867,11 +1148,101 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"node_modules/node-addon-api": { "node_modules/node-addon-api": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="
}, },
"node_modules/nopt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
"dev": true,
"dependencies": {
"abbrev": "1",
"osenv": "^0.1.4"
},
"bin": {
"nopt": "bin/nopt.js"
}
},
"node_modules/normalize-package-data": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
"integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
"dev": true,
"dependencies": {
"hosted-git-info": "^2.1.4",
"resolve": "^1.10.0",
"semver": "2 || 3 || 4 || 5",
"validate-npm-package-license": "^3.0.1"
}
},
"node_modules/npm-normalize-package-bin": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
"integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==",
"dev": true
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"dependencies": {
"wrappy": "1"
}
},
"node_modules/os-homedir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
"integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/osenv": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
"deprecated": "This package is no longer supported.",
"dev": true,
"dependencies": {
"os-homedir": "^1.0.0",
"os-tmpdir": "^1.0.0"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -888,6 +1259,177 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/read-installed": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz",
"integrity": "sha512-O03wg/IYuV/VtnK2h/KXEt9VIbMUFbk3ERG0Iu4FhLZw0EP0T9znqrYDGn6ncbEsXUFaUjiVAWXHzxwt3lhRPQ==",
"deprecated": "This package is no longer supported.",
"dev": true,
"dependencies": {
"debuglog": "^1.0.1",
"read-package-json": "^2.0.0",
"readdir-scoped-modules": "^1.0.0",
"semver": "2 || 3 || 4 || 5",
"slide": "~1.1.3",
"util-extend": "^1.0.1"
},
"optionalDependencies": {
"graceful-fs": "^4.1.2"
}
},
"node_modules/read-package-json": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz",
"integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==",
"deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.",
"dev": true,
"dependencies": {
"glob": "^7.1.1",
"json-parse-even-better-errors": "^2.3.0",
"normalize-package-data": "^2.0.0",
"npm-normalize-package-bin": "^1.0.0"
}
},
"node_modules/readdir-scoped-modules": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz",
"integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==",
"deprecated": "This functionality has been moved to @npmcli/fs",
"dev": true,
"dependencies": {
"debuglog": "^1.0.1",
"dezalgo": "^1.0.0",
"graceful-fs": "^4.1.2",
"once": "^1.3.0"
}
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dev": true,
"dependencies": {
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
"bin": {
"semver": "bin/semver"
}
},
"node_modules/slide": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz",
"integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==",
"dev": true,
"engines": {
"node": "*"
}
},
"node_modules/sortablejs": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz",
"integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A=="
},
"node_modules/spdx-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz",
"integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==",
"dev": true,
"dependencies": {
"array-find-index": "^1.0.2",
"spdx-expression-parse": "^3.0.0",
"spdx-ranges": "^2.0.0"
}
},
"node_modules/spdx-correct": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
"integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
"dev": true,
"dependencies": {
"spdx-expression-parse": "^3.0.0",
"spdx-license-ids": "^3.0.0"
}
},
"node_modules/spdx-exceptions": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
"integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
"dev": true
},
"node_modules/spdx-expression-parse": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
"dev": true,
"dependencies": {
"spdx-exceptions": "^2.1.0",
"spdx-license-ids": "^3.0.0"
}
},
"node_modules/spdx-license-ids": {
"version": "3.0.21",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz",
"integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==",
"dev": true
},
"node_modules/spdx-ranges": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz",
"integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==",
"dev": true
},
"node_modules/spdx-satisfies": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-4.0.1.tgz",
"integrity": "sha512-WVzZ/cXAzoNmjCWiEluEA3BjHp5tiUmmhn9MK+X0tBbR9sOqtC6UQwmgCNrAIZvNlMuBUYAaHYfb2oqlF9SwKA==",
"dev": true,
"dependencies": {
"spdx-compare": "^1.0.0",
"spdx-expression-parse": "^3.0.0",
"spdx-ranges": "^2.0.0"
}
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.5.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.5.tgz",
@@ -911,6 +1453,37 @@
"engines": { "engines": {
"node": ">=8.0" "node": ">=8.0"
} }
},
"node_modules/treeify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz",
"integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==",
"dev": true,
"engines": {
"node": ">=0.6"
}
},
"node_modules/util-extend": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz",
"integrity": "sha512-mLs5zAK+ctllYBj+iAQvlDCwoxU/WDOUaJkcFudeiAX6OajC6BKXJUa9a+tbtkC11dz2Ufb7h0lyvIOVn4LADA==",
"dev": true
},
"node_modules/validate-npm-package-license": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
"dev": true,
"dependencies": {
"spdx-correct": "^3.0.0",
"spdx-expression-parse": "^3.0.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
} }
} }
} }

View File

@@ -11,6 +11,10 @@
"description": "", "description": "",
"dependencies": { "dependencies": {
"@tailwindcss/cli": "^4.1.5", "@tailwindcss/cli": "^4.1.5",
"sortablejs": "^1.15.6",
"tailwindcss": "^4.1.5" "tailwindcss": "^4.1.5"
},
"devDependencies": {
"license-checker": "^25.0.1"
} }
} }

View File

@@ -4,8 +4,9 @@ from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async from channels.db import database_sync_to_async
from django.utils import timezone from django.utils import timezone
from django.urls import reverse from django.urls import reverse
from play.models import QuizGame, QuizGameParticipant, QuizAnswer from play.models import QuizGame, QuizGameParticipant, QuizAnswer,QivipQuiz
from django.conf import settings from django.conf import settings
from geopy.distance import geodesic
class GameConsumer(AsyncWebsocketConsumer): class GameConsumer(AsyncWebsocketConsumer):
@@ -31,6 +32,13 @@ class GameConsumer(AsyncWebsocketConsumer):
# Check if game should be deleted # Check if game should be deleted
await self.cleanup_game() await self.cleanup_game()
@database_sync_to_async
def get_question_data(self):
quiz_game = QuizGame.objects.get(join_code=self.join_code)
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
return json.loads(current_question.data)
async def receive(self, text_data): async def receive(self, text_data):
text_data_json = json.loads(text_data) text_data_json = json.loads(text_data)
message_type = text_data_json['type'] message_type = text_data_json['type']
@@ -84,15 +92,17 @@ class GameConsumer(AsyncWebsocketConsumer):
elif message_type == 'submit_answer': elif message_type == 'submit_answer':
participant_id = text_data_json['participant_id'] participant_id = text_data_json['participant_id']
try:
answer = text_data_json['answer'] answer = text_data_json['answer']
except:
answer=-1
time_remaining = text_data_json.get('time_remaining', 0) time_remaining = text_data_json.get('time_remaining', 0)
# Save answer and update participant score
question_data = await self.get_question_data()
if question_data['type'] == "map":
await self.save_answer_map(participant_id, answer, time_remaining)
else:
await self.save_answer(participant_id, answer, time_remaining) await self.save_answer(participant_id, answer, time_remaining)
# Notify host about the new answer # Notify host about the new answer
await self.channel_layer.group_send( await self.channel_layer.group_send(
self.game_group_name, self.game_group_name,
@@ -111,7 +121,7 @@ class GameConsumer(AsyncWebsocketConsumer):
stats = await self.get_answer_stats() stats = await self.get_answer_stats()
await self.send(text_data=json.dumps({ await self.send(text_data=json.dumps({
'type': 'answer_stats', 'type': 'answer_stats',
'stats': stats 'stats': stats,
})) }))
elif message_type == 'update_participants': elif message_type == 'update_participants':
@@ -128,8 +138,18 @@ class GameConsumer(AsyncWebsocketConsumer):
host_id = text_data_json['host_id'] host_id = text_data_json['host_id']
if await self.verify_host(host_id): if await self.verify_host(host_id):
await self.advance_to_next_question() 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': elif message_type == 'finish_game':
host_id = text_data_json['host_id'] host_id = text_data_json['host_id']
if await self.verify_host(host_id): if await self.verify_host(host_id):
await self.finish_game() await self.finish_game()
@@ -174,47 +194,107 @@ class GameConsumer(AsyncWebsocketConsumer):
return False return False
@database_sync_to_async @database_sync_to_async
def save_answer(self, participant_id, answer_index, time_remaining): def save_answer_map(self, participant_id, answer_location, time_remaining):
answer_index = 0
try: try:
# Get all necessary data first
participant = QuizGameParticipant.objects.get( participant = QuizGameParticipant.objects.get(
quiz_game__join_code=self.join_code, quiz_game__join_code=self.join_code,
participant_id=participant_id participant_id=participant_id
) )
quiz_game = participant.quiz_game quiz_game = participant.quiz_game
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index] current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
# Check if answer is correct
is_correct = False
question_data = json.loads(current_question.data) question_data = json.loads(current_question.data)
print(answer_index) target_location = question_data['target_location']
tolerance_radius = question_data.get('tolerance_radius', 1000) # Default 1km if not set
# Parse coordinates if they are strings
def parse_coords(coord):
if isinstance(coord, str):
try: try:
if answer_index >= 0 : # -1 means timeout # Handle both comma and space separated coordinates
is_correct = question_data['options'][answer_index]['is_correct'] if ',' in coord:
print(is_correct) lat, lng = map(float, [x.strip() for x in coord.split(',')])
except:
user_input = str(answer_index).strip().lower().replace(" ", "")
correct_value = str(question_data['options'][0].get('value', '')).strip().lower().replace(" ", "")
print("Richtig",correct_value)
print(user_input)
if user_input == correct_value:
is_correct=True
answer_index=int(0)
print(is_correct)
else: else:
answer_index=int(1) # Try splitting by space if no comma
parts = coord.strip().split()
if len(parts) >= 2:
lat, lng = map(float, parts[:2])
else:
raise ValueError("Invalid coordinate format")
return (lat, lng)
except (ValueError, AttributeError) as e:
print(f"Error parsing coordinates {coord}: {e}")
return None
elif isinstance(coord, (list, tuple)) and len(coord) >= 2:
return (float(coord[0]), float(coord[1]))
return None
# Parse the coordinates
target_location = parse_coords(target_location)
answer_location_coords = answer_location
answer_location = parse_coords(answer_location)
if not target_location or not answer_location:
print("Invalid coordinates, cannot calculate score")
is_correct = False
# Calculate score based on correctness and time
score = 0 score = 0
if is_correct: else:
base_score = 1000
time_factor = time_remaining / int(current_question.time_per_question)/int(1000)
score = int(base_score * (0.5 + 0.5 * time_factor))
# Update or create answer in QuizAnswer table # Configuration - Make these easily adjustable
MAX_DISTANCE_KM = 50 # Maximum distance for scoring (in km)
DIRECT_HIT_BONUS = 200 # Extra points for direct hit within tolerance
try:
# Calculate distance
distance_km = geodesic(answer_location, target_location).kilometers
tolerance_km = float(tolerance_radius) / 1000 # Convert meters to kilometers
print(f"Distance: {distance_km} km, Tolerance: {tolerance_km} km")
# Calculate base score with non-linear decrease
if distance_km <= tolerance_km:
# Direct hit - full points + bonus
base_score = 1000 + DIRECT_HIT_BONUS
is_correct = True
else:
if distance_km > MAX_DISTANCE_KM:
# Beyond max distance - 0 points
base_score = 0
is_correct = False
else:
# Non-linear decrease using exponential falloff
progress = (distance_km - tolerance_km) / (MAX_DISTANCE_KM - tolerance_km)
falloff = progress ** 1.5 # 1.5 makes the falloff steeper at the beginning
base_score = 1000 * (1 - falloff)
is_correct = True
# Apply time penalty (reduces score based on time taken)
if is_correct and base_score > 0:
# Calculate time taken as a fraction of total time (0-1)
time_taken_sec = (int(current_question.time_per_question) * 1000 - time_remaining) / 1000
max_time_sec = int(current_question.time_per_question)
time_fraction = min(1.0, time_taken_sec / max_time_sec) if max_time_sec > 0 else 1.0
# Reduce points based on time taken (up to 50% reduction for answering at the last second)
time_penalty = base_score * 0.5 * time_fraction # Up to 50% penalty
final_score = max(1, base_score - time_penalty) # At least 1 point if correct
else:
final_score = base_score # 0 points for incorrect answers
# Apply max score limit after all calculations (including bonus)
final_score = min(final_score, 1200) # Cap at 1200 (1000 + 200 bonus)
score = int(round(final_score))
except Exception as e:
print(f"Error calculating score: {e}")
is_correct = False
score = 0
# Save the answer
answer_obj, created = QuizAnswer.objects.get_or_create( answer_obj, created = QuizAnswer.objects.get_or_create(
participant=participant, participant=participant,
question_index=quiz_game.current_question_index, question_index=quiz_game.current_question_index,
@@ -235,6 +315,115 @@ class GameConsumer(AsyncWebsocketConsumer):
participant.last_score = score participant.last_score = score
participant.score += score participant.score += score
participant.coordinate_answer= answer_location_coords
participant.last_answer_correct = is_correct
participant.save()
return True
except (QuizGameParticipant.DoesNotExist, IndexError):
return False
@database_sync_to_async
def save_answer(self, participant_id, answer, time_remaining):
try:
participant = QuizGameParticipant.objects.get(
quiz_game__join_code=self.join_code,
participant_id=participant_id
)
quiz_game = participant.quiz_game
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
question_data = json.loads(current_question.data)
question_type = question_data.get("type")
is_correct = False
answer_index = None
answers_order = None
answer_text = None
if question_type in ["multiple_choice", "true_false"]:
# answer ist hier eine Integer-Antwort (Index)
answer_index = int(answer)
if answer_index >= 0:
is_correct = question_data['options'][answer_index].get('is_correct', False)
elif question_type == "input":
user_input = str(answer).strip().lower().replace(" ", "")
correct_value = str(question_data['options'][0].get('value', '')).strip().lower().replace(" ", "")
is_correct = (user_input == correct_value)
answer_text = user_input
answer_index = 0 if is_correct else 1
elif question_type == "order":
# answer ist hier eine Liste von Strings mit der Reihenfolge
answers_order = answer
correct_sequence = sorted(question_data["options"], key=lambda x: x["order"])
correct_values = [opt["value"] for opt in correct_sequence]
is_correct = (answers_order == correct_values)
elif question_type == "guess":
try:
user_val = float(answer)
correct_val = float(question_data['options'][0].get('value', 0))
tolerance = float(question_data.get('tolerance', 0))
percentage = 1 - abs(user_val - correct_val) / (abs(correct_val) + 1e-5)
score_percentage= min(round(percentage, 3), 1.0)
if score_percentage>= abs(tolerance/100):
is_correct=True
else:
is_correct=False
print(score_percentage)
except ValueError:
is_correct = False # falls keine Zahl eingegeben wurde
answer_index = 0 if is_correct else 1
# Score berechnen
try:
if score_percentage==None:
score_percentage=1
except:
score_percentage=1
score = 0
if is_correct:
base_score = 1000
time_factor = time_remaining / int(current_question.time_per_question) / 1000
score = int(base_score * (0.75 + 0.25 * time_factor))*score_percentage
print(score)
# Antwort speichern oder aktualisieren
answer_obj, created = QuizAnswer.objects.get_or_create(
participant=participant,
question_index=quiz_game.current_question_index,
defaults={
'answer_index': answer_index,
'answers_order': answers_order,
'is_correct': is_correct,
'score': score,
'time_remaining': time_remaining
}
)
if not created:
answer_obj.answer_index = answer_index
answer_obj.answers_order = answers_order
answer_obj.is_correct = is_correct
answer_obj.score = score
answer_obj.time_remaining = time_remaining
answer_obj.save()
# Teilnehmer aktualisieren
participant.last_score = score
participant.score += score
participant.last_answer=answer_index
participant.last_answer_correct = is_correct participant.last_answer_correct = is_correct
participant.save() participant.save()
@@ -242,25 +431,46 @@ class GameConsumer(AsyncWebsocketConsumer):
except (QuizGameParticipant.DoesNotExist, IndexError): except (QuizGameParticipant.DoesNotExist, IndexError):
return False return False
@database_sync_to_async @database_sync_to_async
def get_answer_stats(self): def get_answer_stats(self):
try: try:
quiz_game = QuizGame.objects.get(join_code=self.join_code) quiz_game = QuizGame.objects.get(join_code=self.join_code)
# Count answers for each option in the current question
stats = {}
answers = QuizAnswer.objects.filter( answers = QuizAnswer.objects.filter(
participant__quiz_game=quiz_game, participant__quiz_game=quiz_game,
question_index=quiz_game.current_question_index question_index=quiz_game.current_question_index
) )
for answer in answers:
if answer.answer_index >= 0: # Ignore timeouts (-1)
stats[answer.answer_index] = stats.get(answer.answer_index, 0) + 1
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
question_data = json.loads(current_question.data)
question_type = question_data.get("type")
stats = {}
for single_answer in answers:
if question_type == "order":
correct_answer=[str(opt["value"]) for opt in sorted(question_data["options"], key=lambda x: x["order"])]
if single_answer.answers_order== correct_answer:
key = 0 # richtig
else:
key = 1 # falsch
stats[key] = stats.get(key, 0) + 1
else:
key = str(single_answer.answer_index if single_answer.answer_index is not None else "unanswered")
stats[key] = stats.get(key, 0) + 1
return stats return stats
except QuizGame.DoesNotExist: except:
return {} pass
@database_sync_to_async @database_sync_to_async
def save_rating(self, participant_id, rating): def save_rating(self, participant_id, rating):
@@ -373,6 +583,26 @@ class GameConsumer(AsyncWebsocketConsumer):
'redirect_url': reverse('play:question', kwargs={'join_code': self.join_code}) '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 @database_sync_to_async
def _show_scores(self): def _show_scores(self):
@@ -397,10 +627,34 @@ class GameConsumer(AsyncWebsocketConsumer):
} }
) )
@database_sync_to_async
def counter_played_quizzes(self): # Zähler, wie oft ein Quiz gespielt worden ist.
quiz = QuizGame.objects.get(join_code=self.join_code).quiz_id
quiz_game = QuizGame.objects.get(join_code=self.join_code)
if quiz_game.single_player_or_multiple=="multiple":
quiz.quiz_played_Moderiermodus+=1
active_participants = QuizGameParticipant.objects.filter(
quiz_game=quiz_game
).count()
quiz.quiz_played_Moderiermodus_players+=active_participants
else:
quiz.quiz_played_Lernmodus+=1
quiz.save()
@database_sync_to_async @database_sync_to_async
def _finish_game(self): def _finish_game(self):
try: try:
quiz_game = QuizGame.objects.get(join_code=self.join_code) quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz_game.current_state = 'finished' quiz_game.current_state = 'finished'
quiz_game.save() quiz_game.save()
return True return True
@@ -409,6 +663,7 @@ class GameConsumer(AsyncWebsocketConsumer):
async def finish_game(self): async def finish_game(self):
success = await self._finish_game() success = await self._finish_game()
await self.counter_played_quizzes()
if success: if success:
# Notify clients to redirect to finished page # Notify clients to redirect to finished page
await self.channel_layer.group_send( await self.channel_layer.group_send(
@@ -419,3 +674,48 @@ class GameConsumer(AsyncWebsocketConsumer):
'redirect_url': reverse('play:finished', kwargs={'join_code': self.join_code}) '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,28 @@
# Generated by Django 5.1.7 on 2025-07-18 13:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0012_quizgameparticipant_last_score'),
]
operations = [
migrations.AddField(
model_name='quizanswer',
name='answer_text',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='quizanswer',
name='answers_order',
field=models.JSONField(blank=True, null=True),
),
migrations.AlterField(
model_name='quizanswer',
name='answer_index',
field=models.IntegerField(blank=True, null=True),
),
]

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

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-11-01 15:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0015_alter_quizgame_current_state'),
]
operations = [
migrations.AddField(
model_name='quizgameparticipant',
name='coordinate_answer',
field=models.CharField(blank=True, max_length=256, null=True),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.1.7 on 2025-11-14 13:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0016_quizgameparticipant_coordinate_answer'),
]
operations = [
migrations.AddField(
model_name='quizgame',
name='quiz_played_Lernmodus',
field=models.IntegerField(blank=True, default=0, null=True),
),
migrations.AddField(
model_name='quizgame',
name='quiz_played_Moderiermodus',
field=models.IntegerField(blank=True, default=0, null=True),
),
migrations.AddField(
model_name='quizgame',
name='quiz_played_Moderiermodus_players',
field=models.IntegerField(blank=True, default=0, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-11-14 14:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0017_quizgame_quiz_played_lernmodus_and_more'),
]
operations = [
migrations.AddField(
model_name='quizgame',
name='single_player_or_multiple',
field=models.CharField(blank=True, max_length=20),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 5.1.7 on 2025-11-14 14:50
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('play', '0018_quizgame_single_player_or_multiple'),
]
operations = [
migrations.RemoveField(
model_name='quizgame',
name='quiz_played_Lernmodus',
),
migrations.RemoveField(
model_name='quizgame',
name='quiz_played_Moderiermodus',
),
migrations.RemoveField(
model_name='quizgame',
name='quiz_played_Moderiermodus_players',
),
migrations.RemoveField(
model_name='quizgame',
name='single_player_or_multiple',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-11-14 15:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0019_remove_quizgame_quiz_played_lernmodus_and_more'),
]
operations = [
migrations.AddField(
model_name='quizgame',
name='single_player_or_multiple',
field=models.CharField(blank=True, max_length=20),
),
]

View File

@@ -10,9 +10,12 @@ class QuizGame(models.Model):
('lobby', 'In Lobby'), ('lobby', 'In Lobby'),
('question', 'Frage läuft'), ('question', 'Frage läuft'),
('scores', 'Punkteübersicht'), ('scores', 'Punkteübersicht'),
('finished', 'Beendet') ('winner_podest','Siegerpodest'),
] ('finished', 'Beendet'),
('scoreboard', 'Scoreboard'),
]
single_player_or_multiple=models.CharField(max_length=20, blank=True)
host_id = models.CharField(max_length=200, unique=True) host_id = models.CharField(max_length=200, unique=True)
join_code = models.CharField(max_length=6, unique=True, blank=True) join_code = models.CharField(max_length=6, unique=True, blank=True)
quiz_id = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE) quiz_id = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE)
@@ -47,6 +50,7 @@ class QuizGameParticipant(models.Model):
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)
coordinate_answer = models.CharField(null=True, blank=True,max_length=256)
last_answer_correct = models.BooleanField(null=True, blank=True) last_answer_correct = models.BooleanField(null=True, blank=True)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@@ -63,7 +67,9 @@ class QuizGameParticipant(models.Model):
class QuizAnswer(models.Model): class QuizAnswer(models.Model):
participant = models.ForeignKey(QuizGameParticipant, on_delete=models.CASCADE, related_name='answers') participant = models.ForeignKey(QuizGameParticipant, on_delete=models.CASCADE, related_name='answers')
question_index = models.IntegerField() question_index = models.IntegerField()
answer_index = models.IntegerField() answer_index = models.IntegerField(null=True, blank=True) # für MC, TF
answer_text = models.TextField(null=True, blank=True) # für Input
answers_order = models.JSONField(null=True, blank=True) # NEU für "order"
is_correct = models.BooleanField() is_correct = models.BooleanField()
score = models.IntegerField() score = models.IntegerField()
time_remaining = models.IntegerField() time_remaining = models.IntegerField()

View File

@@ -12,6 +12,8 @@ urlpatterns = [
path('game/<str:join_code>/waiting', views.waiting_room, name='waiting'), 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>/scores', views.scores, name='scores'),
path('game/<str:join_code>/finished', views.finished, name='finished'), 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('select_mode/<str:join_code>', views.select_mode, name='select_mode'),
path('selected_mode/<str:join_code>', views.selected_mode, name='selected_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, 'question_index': quiz_game.current_question_index,
'total_questions': len(questions) '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): 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): def finished(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=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') return redirect('home')
def question(request, join_code): def question(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code) quiz_game = get_object_or_404(QuizGame, join_code=join_code)
@@ -339,13 +420,38 @@ def waiting_room(request, join_code):
def selected_mode(request, join_code): def selected_mode(request, join_code):
mode = request.GET.get('mode','default') mode = request.GET.get('mode','default')
request.session['mode'] = mode request.session['mode'] = mode
quiz_game = QuizGame.objects.get(join_code=join_code)
if mode=="learn":
quiz_game.single_player_or_multiple="single"
else:
quiz_game.single_player_or_multiple="multiple"
quiz_game.save()
quiz_game = get_object_or_404(QuizGame, join_code=join_code) quiz_game = get_object_or_404(QuizGame, join_code=join_code)
quiz = get_object_or_404(QivipQuiz, pk=quiz_game.quiz_id.id) quiz = get_object_or_404(QivipQuiz, pk=quiz_game.quiz_id.id)
QuizGameParticipant.objects.filter(quiz_game=quiz_game).delete() QuizGameParticipant.objects.filter(quiz_game=quiz_game).delete()
# Hier werden die Fragen gemischt!
import random
questions = quiz_game.quiz_id.questions.all()
for question in questions:
try:
data = json.loads(question.data)
if (data.get('type') == 'order' or data.get('type') == 'multiple_choice') and 'options' in data:
random.shuffle(data['options'])
question.data = json.dumps(data)
question.save(update_fields=['data'])
except Exception:
pass
context = { context = {
'quiz': quiz, 'quiz': quiz,
'quiz_game': quiz_game, 'quiz_game': quiz_game,

View File

@@ -3,6 +3,9 @@
qp-container { qp-container {
@apply max-w-xl w-full mx-auto; @apply max-w-xl w-full mx-auto;
} }
.dark-form label {
@apply dark:text-white!;
}
nav div ul.qp-nav-list li { nav div ul.qp-nav-list li {
@apply text-white hover:scale-110 hover:border-b-2 hover:border-white transition duration-200; @apply text-white hover:scale-110 hover:border-b-2 hover:border-white transition duration-200;
@@ -53,6 +56,34 @@ a.qp-a-button-small {
} }
.field-group form p {
@apply flex flex-col gap-2;
}
.field-group form p label {
@apply text-gray-900 font-semibold text-sm;
}
.field-group form p input {
@apply p-3 rounded-full bg-gray-200 focus:scale-105 transition duration-200 font-black
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100;
}
.field-group form p select {
@apply p-3 rounded-md bg-gray-200 focus:scale-105 transition duration-200 font-black
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100;
}
.field-group form p textarea {
@apply p-3 rounded-xl bg-gray-200 focus:scale-105 transition duration-200 font-black
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100 min-h-[100px];
}
.field-group form p button {
@apply p-3 rounded-full bg-indigo-500 hover:bg-indigo-600 text-white focus:scale-105
transition duration-200 font-black shadow-md hover:shadow-lg;
}
ul.messages li { ul.messages li {
@@ -122,3 +153,12 @@ div.qp-question-container {
div.qp-question-container img { div.qp-question-container img {
@apply max-w-[50vw] max-h-[40vh] mx-auto; @apply max-w-[50vw] max-h-[40vh] mx-auto;
} }
div#mapquestion-container {
display: flex;
justify-content: center;
}
div#map {
@apply md:w-[600px] lg:w-[900px] h-[400px]
}

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
<div class="max-w-2xl mx-auto mt-20 p-6 bg-white shadow-xl rounded-2xl text-center"> <div class="max-w-2xl mx-auto mt-20 p-6 bg-white shadow-xl rounded-2xl text-center dark:bg-transparent">
<div class="flex justify-center items-center gap-4"> <div class="flex justify-center items-center gap-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
@@ -21,8 +21,8 @@
</svg> </svg>
</div> </div>
<h2 class="text-2xl font-semibold text-gray-800 mb-2 ">Seite nicht gefunden</h2> <h2 class="text-2xl font-semibold text-gray-800 mb-2 dark:text-white ">Seite nicht gefunden</h2>
<p class="text-lg text-gray-600 mb-6">Ups, die angeforderte Seite wurde nicht gefunden. Überprüfen Sie die URL oder kehren Sie zur Startseite zurück.</p> <p class="text-lg text-gray-600 mb-6 dark:text-white">Ups, die angeforderte Seite wurde nicht gefunden. Überprüfen Sie die URL oder kehren Sie zur Startseite zurück.</p>
<div class="flex justify-center"> <div class="flex justify-center">
<a href="{% url 'homepage:home' %}" <a href="{% url 'homepage:home' %}"

View File

@@ -3,8 +3,8 @@
{% block content %} {% block content %}
<div class="flex justify-center items-center mt-12 "> <div class="flex justify-center items-center mt-12">
<div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md"> <div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent! dark:text-white! ">
<div class="grid place-content-center h-full mb-4 flex-col items-center justify-between"> <div class="grid place-content-center h-full mb-4 flex-col items-center justify-between">
<h1 class="text-center m-4 text-3xl lg:text-6xl">Willkommen, <b>{{ request.user.username }}</b>!</h1> <h1 class="text-center m-4 text-3xl lg:text-6xl">Willkommen, <b>{{ request.user.username }}</b>!</h1>
@@ -18,16 +18,16 @@
<button class="text-center text-sm w-full mt-4 font-light text-red-600"><a href="{% url 'accounts:delete_account' %}">Account löschen</a></button> <button class="text-center text-sm w-full mt-4 font-light text-red-600"><a href="{% url 'accounts:delete_account' %}">Account löschen</a></button>
<div class="border-2 border-blue-600 p-6 rounded-xl shadow-inner bg-blue-50 flex flex-col gap-4 items-start mt-8"> <div class="border-2 border-blue-600 p-6 rounded-xl shadow-inner bg-blue-50 flex flex-col gap-4 items-start mt-8 dark:bg-transparent">
<div class="flex items-center gap-2 text-xl font-semibold text-blue-800"> <div class="flex items-center gap-2 text-xl font-semibold text-blue-800 dark: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="w-6 h-6 text-yellow-500"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-yellow-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 18.75h-9m9 0a3 3 0 0 1 3 3h-15a3 3 0 0 1 3-3m9 0v-3.375c0-.621-.503-1.125-1.125-1.125h-.871M7.5 18.75v-3.375c0-.621.504-1.125 1.125-1.125h.872m5.007 0H9.497m5.007 0a7.454 7.454 0 0 1-.982-3.172M9.497 14.25a7.454 7.454 0 0 0 .981-3.172M5.25 4.236c-.982.143-1.954.317-2.916.52A6.003 6.003 0 0 0 7.73 9.728M5.25 4.236V4.5c0 2.108.966 3.99 2.48 5.228M5.25 4.236V2.721C7.456 2.41 9.71 2.25 12 2.25c2.291 0 4.545.16 6.75.47v1.516M7.73 9.728a6.726 6.726 0 0 0 2.748 1.35m8.272-6.842V4.5c0 2.108-.966 3.99-2.48 5.228m2.48-5.492a46.32 46.32 0 0 1 2.916.52 6.003 6.003 0 0 1-5.395 4.972m0 0a6.726 6.726 0 0 1-2.749 1.35m0 0a6.772 6.772 0 0 1-3.044 0" /> <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 18.75h-9m9 0a3 3 0 0 1 3 3h-15a3 3 0 0 1 3-3m9 0v-3.375c0-.621-.503-1.125-1.125-1.125h-.871M7.5 18.75v-3.375c0-.621.504-1.125 1.125-1.125h.872m5.007 0H9.497m5.007 0a7.454 7.454 0 0 1-.982-3.172M9.497 14.25a7.454 7.454 0 0 0 .981-3.172M5.25 4.236c-.982.143-1.954.317-2.916.52A6.003 6.003 0 0 0 7.73 9.728M5.25 4.236V4.5c0 2.108.966 3.99 2.48 5.228M5.25 4.236V2.721C7.456 2.41 9.71 2.25 12 2.25c2.291 0 4.545.16 6.75.47v1.516M7.73 9.728a6.726 6.726 0 0 0 2.748 1.35m8.272-6.842V4.5c0 2.108-.966 3.99-2.48 5.228m2.48-5.492a46.32 46.32 0 0 1 2.916.52 6.003 6.003 0 0 1-5.395 4.972m0 0a6.726 6.726 0 0 1-2.749 1.35m0 0a6.772 6.772 0 0 1-3.044 0" />
</svg> </svg>
Erfolge Erfolge
</div> </div>
<ul class="list-disc pl-6 text-base text-gray-700"> <ul class="list-disc pl-6 text-base text-gray-700 dark:text-white">
<li><b>{{ my_quizzes_number }}</b> Quizze erstellt</li> <li><b>{{ my_quizzes_number }}</b> Quizze erstellt</li>
<li><b>{{ my_questions_number }}</b> Fragen insgesamt erstellt</li> <li><b>{{ my_questions_number }}</b> Fragen insgesamt erstellt</li>
</ul> </ul>
@@ -37,7 +37,7 @@
</div> </div>
<div class="flex justify-end"> <div class="flex justify-end">
<span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white">qivip <span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white dark:bg-transparent">qivip
</span> </span>
</div> </div>
</div> </div>

View File

@@ -4,7 +4,7 @@
{% block content %} {% block content %}
<div class="grid place-content-center h-120 mt-12 mb-12 "> <div class="grid place-content-center h-120 mt-12 mb-12 ">
<div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90"> <div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90">
<h2 class="text-2xl text-center p-4 font-black">Anmeldung</h2> <h2 class="text-2xl text-center p-4 font-black dark:text-white">Anmeldung</h2>
<form class="space-y-4" method="post"> <form class="space-y-4" method="post">
{% if form.errors %} {% if form.errors %}
<div class=" items-center text-red-600 font-black overflow-x-auto break-words text-center"> <div class=" items-center text-red-600 font-black overflow-x-auto break-words text-center">

View File

@@ -4,7 +4,7 @@
<div class="grid place-content-center h-120 mt-12 mb-12"> <div class="grid place-content-center h-120 mt-12 mb-12">
<div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90"> <div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90">
<h2 class="text-2xl text-center p-4 font-black">Registrieren</h2> <h2 class="text-2xl text-center p-4 font-black dark:text-white">Registrieren</h2>
<form class=" space-y-4" method="post"> <form class=" space-y-4" method="post">
{% if form.errors %} {% if form.errors %}

View File

@@ -1,6 +1,6 @@
{% load static %} {% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de" class="dark:bg-gray-900 h-full">
<head> <head>
<link rel="icon" type="image/png" href="{% static 'icons/favicon.png' %}"> <link rel="icon" type="image/png" href="{% static 'icons/favicon.png' %}">
<meta charset="UTF-8"> <meta charset="UTF-8">
@@ -9,11 +9,16 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.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> <script src="https://cdn.jsdelivr.net/npm/@tsparticles/confetti@3.0.3/tsparticles.confetti.bundle.min.js"></script>
<title>qivip</title> <title>qivip</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
</head> </head>
<body> <body class="flex flex-col min-h-screen">
<main class="grow">
{% include 'partials/_nav.html' %} {% include 'partials/_nav.html' %}
{% block content%}{% endblock %} {% block content%}{% endblock %}
{% block faqs%}{% endblock %} {% block faqs%}{% endblock %}
</main>
{% include 'partials/_footer.html' %} {% include 'partials/_footer.html' %}
<!-- Toast Container --> <!-- Toast Container -->
@@ -68,12 +73,12 @@
}, duration); }, duration);
} }
}; };
if (window.location.pathname !== "/library/") {
localStorage.setItem("meine_seite_scroll", 0);}
</script> </script>
{% block extra_js %}{% endblock %} {% block extra_js %}{% endblock %}
<style>html { <style>html {
overflow-y: scroll; overflow-y: scroll;
}</style> }</style>
</body> </body>
</html> </html>

View File

@@ -4,9 +4,9 @@
<div class="grid place-content-center md:h-screen h-150 w-full -z-50 top-0 p-4"> <div class="grid place-content-center md:h-screen h-150 w-full -z-50 top-0 p-4">
<div class="text-center"> <div class="text-center">
<h2 class="font-bold md:text-8xl text-6xl md:mb-8 text-blue-600">qivip</h2> <h2 class="font-bold md:text-8xl text-6xl md:mb-8 text-blue-600">qivip</h2>
<h3 class="mt-4 italic font-extralight sm:text-4xl text-md">weil Lernen auch Spaß <br> machen kann!</h3> <h3 class="mt-4 italic font-light sm:text-4xl text-md bg-gradient-to-r from-pink-500 via-purple-600 to-blue-700 bg-clip-text text-transparent">weil Lernen auch Spaß <br> machen kann!</h3>
</div> </div>
<div class="grid sm:grid-cols-3 place-items-stretch text-center mt-8 gap-4 p-4 border-3 bg-blue-100 border-blue-100 rounded-md"> <div class="grid sm:grid-cols-3 place-items-stretch text-center mt-8 gap-4 p-4 border-3 bg-blue-100 border-blue-100 rounded-md dark:bg-gray-900 dark:border-0">
<a class="qp-a-button bg-green-500 text-white" href="{% url 'play:join_game' %}">Teilnehmen</a> <a class="qp-a-button bg-green-500 text-white" href="{% url 'play:join_game' %}">Teilnehmen</a>
<a class="qp-a-button bg-indigo-500 text-white" href="{% url 'library:overview_quiz' %}">Moderieren</a> <a class="qp-a-button bg-indigo-500 text-white" href="{% url 'library:overview_quiz' %}">Moderieren</a>
<a class="qp-a-button bg-purple-500 text-white" href="{% url 'library:new_quiz' %}">Erstellen</a> <a class="qp-a-button bg-purple-500 text-white" href="{% url 'library:new_quiz' %}">Erstellen</a>
@@ -25,13 +25,13 @@
{% if faqs %} {% if faqs %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6"> <div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<h2 class="text-2xl font-bold text-gray-900">FAQs</h2> <h2 class="text-2xl font-bold text-gray-900 dark:text-white">FAQs</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div> <div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div> </div>
</div> </div>
<div class="input-group !max-w-4xl tracking-wide"> <div class="input-group !max-w-4xl tracking-wide dark:!bg-gray-900 dark:!text-white">
<h1 class="font-black text-blue-600 flex justify-between gap-2 lg:text-lg"> <h1 class="font-black text-blue-600 flex justify-between gap-2 lg:text-lg">
@@ -43,7 +43,7 @@
</div> </div>
{% for faq in faqs %} {% for faq in faqs %}
<div class="input-group mt-2.5 !max-w-4xl hover:!bg-gray-50"> <div class="input-group mt-2.5 !max-w-4xl hover:bg-gray-50 dark:!bg-gray-900 dark:!text-white ">
<details> <details>
<summary class="flex justify-between cursor-pointer font-bold">{{ faq.question }} <summary class="flex justify-between cursor-pointer font-bold">{{ faq.question }}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7">

View File

@@ -1,8 +1,8 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content%} {% block content%}
<div class="flex justify-center items-center mt-12 "> <div class="flex justify-center items-center mt-12">
<div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md"> <div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md dark:!bg-transparent dark:!text-white ">
<div class="grid place-content-center h-120"> <div class="grid place-content-center h-120">
<h1 class="text-center m-4 text-3xl lg:text-6xl">Impressum</b></h1> <h1 class="text-center m-4 text-3xl lg:text-6xl">Impressum</b></h1>
@@ -12,7 +12,7 @@
</div> </div>
<div class="flex justify-end"> <div class="flex justify-end">
<span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white">qivip <span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white dark:bg-transparent ">qivip
</span> </span>
</div> </div>
</div> </div>

View File

@@ -2,7 +2,7 @@
{% block content%} {% block content%}
<div class="flex justify-center items-center mt-12 "> <div class="flex justify-center items-center mt-12 ">
<div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md"> <div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md dark:!bg-transparent dark:!text-white">
<div class="grid place-content-center h-120"> <div class="grid place-content-center h-120">
<h1 class="text-center m-4 text-3xl lg:text-6xl">Datenschutz-</br>erklärung</h1> <h1 class="text-center m-4 text-3xl lg:text-6xl">Datenschutz-</br>erklärung</h1>
@@ -12,7 +12,7 @@
</div> </div>
<div class="flex justify-end"> <div class="flex justify-end">
<span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white">qivip <span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white dark:!bg-transparent ">qivip
</span> </span>
</div> </div>
</div> </div>

View File

@@ -3,15 +3,15 @@
{% block content %} {% block content %}
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="max-w-lg mx-auto mt-10"> <div class="max-w-lg mx-auto mt-10">
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md"> <div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:text-white dark:bg-transparent">
<div class="text-center"> <div class="text-center">
<svg class="mx-auto h-12 w-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="mx-auto h-12 w-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg> </svg>
<h2 class="mt-4 text-xl font-bold text-gray-900">Löschen bestätigen</h2> <h2 class="mt-4 text-xl font-bold text-gray-900 dark:text-white">Löschen bestätigen</h2>
<p class="mt-2 text-sm text-gray-600"> <p class="mt-2 text-sm text-gray-600 dark:text-white">
{% if object.quiz_id %} {% if object.quiz_id %}
Möchten Sie die Frage "{{ object.data.question }}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden. Möchten Sie die Frage "{{ object.data.question }}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.
{% else %} {% else %}

View File

@@ -3,22 +3,21 @@
{% block content %} {% block content %}
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex flex-wrap space-y-2 items-center justify-between mb-6 dark:text-white">
<div class="flex flex-wrap space-y-2 items-center justify-between mb-6"> <h1 class="text-2xl font-bold mr-4 dark:text-white ">{{ quiz.name }}</h1>
<h1 class="text-2xl font-bold mr-4 ">{{ quiz.name }}</h1>
<div class=" flex flex-wrap space-x-2 "> <div class=" flex flex-wrap space-x-2 ">
<div class="flex flex-wrap"> <div class="flex flex-wrap">
{% if quiz.user_id == request.user %} {% if quiz.user_id == request.user %}
<a href="{% url 'library:edit_quiz' quiz.id %}" class=" text-white px-2 py-2 rounded-md text-sm lg:text-lg hover:scale-110 transition-colors"> <a href="{% url 'library:edit_quiz' quiz.id %}" class=" text-white px-2 py-2 rounded-md text-sm lg:text-lg hover:scale-110 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7 text-gray-600"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7 text-gray-600 dark:text-white">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" /> <path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg> </svg>
</a> </a>
<a href="{% url 'library:delete_quiz' quiz.id %}" class=" text-white px-2 py-2 rounded-md text-sm lg:text-lg hover:scale-110 transition-colors"> <a href="{% url 'library:delete_quiz' quiz.id %}" class=" text-white px-2 py-2 rounded-md text-sm lg:text-lg hover:scale-110 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7 text-gray-600"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7 text-gray-600 dark:text-white">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" /> <path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg> </svg>
</a> </a>
@@ -32,7 +31,7 @@
<path stroke-linecap="round" stroke-linejoin="round" <path stroke-linecap="round" stroke-linejoin="round"
d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m8.9-4.414c.376.023.75.05 1.124.08 1.131.094 1.976 1.057 1.976 2.192V16.5A2.25 2.25 0 0 1 18 18.75h-2.25m-7.5-10.5H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V18.75m-7.5-10.5h6.375c.621 0 1.125.504 1.125 1.125v9.375m-8.25-3 1.5 1.5 3-3.75" /> d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m8.9-4.414c.376.023.75.05 1.124.08 1.131.094 1.976 1.057 1.976 2.192V16.5A2.25 2.25 0 0 1 18 18.75h-2.25m-7.5-10.5H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V18.75m-7.5-10.5h6.375c.621 0 1.125.504 1.125 1.125v9.375m-8.25-3 1.5 1.5 3-3.75" />
</svg> </svg>
Quiz kopieren Quiz duplizieren
</a> </a>
{% if quiz.questions.count != 0 %} {% if quiz.questions.count != 0 %}
@@ -51,16 +50,17 @@
</div> </div>
</div> </div>
{% if quiz.description %} {% if quiz.description %}
<p class="text-gray-600 mb-4">{{ quiz.description }}</p> <p class="text-gray-600 mb-4 dark:text-gray-400">{{ quiz.description }}</p>
{% endif %} {% endif %}
<div class="flex justify-end mb-4"> <div class="flex justify-end mb-4">
{% if detail == True %} {% if detail == True %}
<a href="{% url 'library:detail_quiz' quiz.id %}?show_answers=false"> <a href="{% url 'library:detail_quiz' quiz.id %}?show_answers=false">
<div class="flex items-center mr-2"> <div class="flex items-center mr-2 dark:text-white">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" /> <path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" />
</svg> </svg>
@@ -71,7 +71,7 @@
{% else %} {% else %}
<a href="{% url 'library:detail_quiz' quiz.id %}?show_answers=true"> <a href="{% url 'library:detail_quiz' quiz.id %}?show_answers=true">
<div class="flex items-center mr-2"> <div class="flex items-center mr-2 dark:text-white">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
@@ -85,7 +85,7 @@
</div> </div>
<div class="bg-white rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md"> <div class="bg-white rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md dark:!bg-transparent dark:text-white">
<div class="border-b border-gray-200 p-4"> <div class="border-b border-gray-200 p-4">
<div class="flex justify-between items-center flex-wrap"> <div class="flex justify-between items-center flex-wrap">
<h2 class="text-xl font-semibold">Fragen <h2 class="text-xl font-semibold">Fragen
@@ -97,20 +97,31 @@
class=" mt-1 bg-green-100 text-green-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2"> class=" mt-1 bg-green-100 text-green-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
Eingabe Eingabe
</a> </a>
{% endif %}
{% if quiz.user_id == request.user %}
<a href="{% url 'library:new_question' %}?type=multiple_choice&quiz_id={{ quiz.id }}" <a href="{% url 'library:new_question' %}?type=multiple_choice&quiz_id={{ quiz.id }}"
class=" mt-1 bg-blue-100 text-blue-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2"> class=" mt-1 bg-blue-100 text-blue-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
Multiple Choice Multiple Choice
</a> </a>
{% endif %}
{% if quiz.user_id == request.user %}
<a href="{% url 'library:new_question' %}?type=true_false&quiz_id={{ quiz.id }}" <a href="{% url 'library:new_question' %}?type=true_false&quiz_id={{ quiz.id }}"
class=" mt-1 bg-purple-100 text-purple-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2"> class=" mt-1 bg-purple-100 text-purple-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
Wahr/Falsch Wahr/Falsch
</a> </a>
<a href="{% url 'library:new_question' %}?type=order&quiz_id={{ quiz.id }}"
class=" mt-1 bg-yellow-100 text-yellow-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
Reihenfolge
</a>
<a href="{% url 'library:new_question' %}?type=guess&quiz_id={{ quiz.id }}"
class=" mt-1 bg-pink-100 text-pink-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
Schätzen
</a>
<a href="{% url 'library:new_question' %}?type=map&quiz_id={{ quiz.id }}"
class=" mt-1 bg-orange-100 text-orange-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
Karte
</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -123,7 +134,7 @@
<div class="w-full rounded-xl p-4 hover:bg-gray-50"> <div class="w-full rounded-xl p-4 hover:bg-gray-50 dark:hover:bg-[#2a2f3a] dark:text-white">
<p>{{ forloop.counter }}.</p> <p>{{ forloop.counter }}.</p>
{% if quiz.user_id == request.user %} <a class="block flex h-full w-full" href="{% url 'library:edit_question' question.id %}" > {% endif %} {% if quiz.user_id == request.user %} <a class="block flex h-full w-full" href="{% url 'library:edit_question' question.id %}" > {% endif %}
<div class="flex justify-between items-start"> <div class="flex justify-between items-start">
@@ -131,8 +142,14 @@
<div class="flex-grow"> <div class="flex-grow">
<p class="font-medium">{{ question.data.question }}</p> <p class="font-medium">{{ question.data.question }}</p>
<div class="mt-1"> <div class="mt-1">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {% if question.data.type == 'multiple_choice' %}bg-blue-100 text-blue-800 {% elif question.data.type == 'input' %} bg-green-100 text-green-800 {% else %}bg-purple-100 text-purple-800{% endif %}"> <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {% if question.data.type == 'multiple_choice' %}bg-blue-100 text-blue-800 {% elif question.data.type == 'input' %} bg-green-100 text-green-800 {% elif question.data.type == 'map' %}bg-orange-100 text-orange-800 {% elif question.data.type == 'true_false' %}bg-purple-100 text-purple-800{% elif question.data.type == 'order' %}bg-yellow-100 text-yellow-800{% else %}bg-pink-100 text-pink-800 {% endif %}">
{% if question.data.type == 'multiple_choice' %}Multiple Choice{% elif question.data.type == 'input' %}Eingabe{% else %}Wahr/Falsch{% endif %} {% if question.data.type == 'multiple_choice' %}Multiple Choice
{% elif question.data.type == 'input' %}Eingabe
{% elif question.data.type == 'map' %}Karte
{% elif question.data.type == 'true_false' %}Wahr/Falsch
{% elif question.data.type == 'order' %}Reihenfolge
{% else %}Schätzen
{% endif %}
</span> </span>
</div> </div>
<div class="mt-2"> <div class="mt-2">
@@ -141,10 +158,10 @@
{% if question.data.type == 'multiple_choice' %} {% if question.data.type == 'multiple_choice' %}
<div class="w-full gap-4 md:flex justify-between "> <div class="w-full gap-4 md:flex justify-between ">
<ul class="space-y-1 "> <ul class="space-y-1 ">
{% for option in question.data.options %} {% for option in question.data.options|dictsort:"order" %}
<li class="flex items-center text-sm"> <li class="flex items-center text-sm ">
{% if option.is_correct %} {% if option.is_correct %}
<svg class="h-4 w-4 text-green-500 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-4 w-4 text-green-500 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -152,10 +169,10 @@
</svg> </svg>
<span class="font-medium text-green-700">{{ option.value }}</span> <span class="font-medium text-green-700">{{ option.value }}</span>
{% else %} {% else %}
<svg class="h-4 w-4 text-gray-400 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-4 w-4 text-gray-400 mr-1.5 dark:text-white " fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 12H6"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 12H6"/>
</svg> </svg>
<span class="text-gray-600">{{ option.value }}</span> <span class="text-gray-600 dark:text-white">{{ option.value }}</span>
{% endif %} {% endif %}
</li> </li>
{% endfor %} {% endfor %}
@@ -165,6 +182,58 @@
{% endif %} {% endif %}
</div> </div>
{% elif question.data.type == 'map' and detail %}
<div class="mt-3">
<div class="text-sm text-gray-600 dark:text-gray-300">
<span class="font-medium">Zielort:</span>
<span id="target-location-{{ forloop.counter }}">
{% if question.data.target_location %}
{{ question.data.target_location }}
{% else %}
Kein Zielort festgelegt
{% endif %}
</span>
</div>
{% if question.data.target_location %}
<script>
// Format the coordinates to 4 decimal places
document.addEventListener('DOMContentLoaded', function() {
const targetEl = document.getElementById('target-location-{{ forloop.counter }}');
if (targetEl) {
const coords = targetEl.textContent.trim().split(',');
if (coords.length === 2) {
const lat = parseFloat(coords[0]).toFixed(4);
const lng = parseFloat(coords[1]).toFixed(4);
targetEl.textContent = `${lat}, ${lng}`;
}
}
});
</script>
{% endif %}
{% if question.data.tolerance_radius %}
<div class="text-sm text-gray-600 dark:text-gray-300 mt-1">
<span class="font-medium">Toleranzradius:</span> {{ question.data.tolerance_radius }} Meter
</div>
{% endif %}
{% if question.image.image.url %}
<img src="{{ question.image.image.url }}" alt="Bild der Frage" class="w-[200px] rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md object-contain mt-4" />
{% endif %}
</div>
{% elif question.data.type == 'order' %}
<div class="w-full gap-4 md:flex justify-between ">
<ul class="space-y-1 ">
{% for option in question.data.options|dictsort:"order" %}
<li class="flex items-center text-sm">
{{ forloop.counter }}. &nbsp;
<span class="font-medium text-green-700 dark:text-white">{{ option.value }}</span>
</li>
{% endfor %}
</ul>
{% if question.image.image.url %}
<img src="{{ question.image.image.url }}" alt="Bild der Frage" class="w-[200px] rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md object-contain mt-4 md:mt-0" />
{% endif %}
</div>
{% elif question.data.type == 'input' %} {% elif question.data.type == 'input' %}
<div class="w-full gap-4 md:flex justify-between "> <div class="w-full gap-4 md:flex justify-between ">
<ul class="space-y-1 "> <ul class="space-y-1 ">
@@ -173,7 +242,23 @@
<svg class="h-4 w-4 text-green-500 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-4 w-4 text-green-500 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg> </svg>
<span class="font-medium text-green-700">{{ option.value }}</span> <span class="font-medium text-green-700 dark:text-white">{{ option.value }}</span>
</li>
{% endfor %}
</ul>
{% if question.image.image.url %}
<img src="{{ question.image.image.url }}" alt="Bild der Frage" class="w-[200px] rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md object-contain mt-4 md:mt-0" />
{% endif %}
</div>
{% elif question.data.type == 'guess' %}
<div class="w-full gap-4 md:flex justify-between ">
<ul class="space-y-1 ">
{% for option in question.data.options %}
<li class="flex items-center text-sm">
<svg class="h-4 w-4 text-green-500 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span class="font-medium text-green-700 dark:text-white">{{ option.value }}</span>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
@@ -186,7 +271,7 @@
{% for option in question.data.options %} {% for option in question.data.options %}
{% if option.is_correct %} {% if option.is_correct %}
<p class="text-sm"> <p class="text-sm">
<span class="text-gray-500">Richtige Antwort:</span> <span class="text-gray-500 dark:text-white">Richtige Antwort:</span>
<span class="ml-1 font-medium {% if option.value == "Wahr" %} text-green-700 {% else %}text-red-700{% endif %}">{{ option.value }}</span> <span class="ml-1 font-medium {% if option.value == "Wahr" %} text-green-700 {% else %}text-red-700{% endif %}">{{ option.value }}</span>
</p> </p>
{% endif %} {% endif %}
@@ -209,7 +294,7 @@
<a href=" {% url 'library:delete_question' question.id %}" <a href=" {% url 'library:delete_question' question.id %}"
class=" text-red-700 px-4 py-1 rounded hover:scale-110 "> class=" text-red-700 px-4 py-1 rounded hover:scale-110 ">
{% if quiz.user_id == request.user %} {% if quiz.user_id == request.user %}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7 text-gray-600"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="dark:text-white size-7 text-gray-600">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" /> <path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg> </svg>
{% endif %} {% endif %}
@@ -236,44 +321,125 @@
<div class="flex justify-end"> <div class="flex justify-end">
{% if quiz.base_quiz %} {% if quiz.base_quiz %}
<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> <p class="text-gray-600 mt-2 dark:text-white">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 %} {% 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> <p class="text-gray-600 mt-2 dark:text-white">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 %} {% endif %}
</div> </div>
<div class="flex justify-end"> <div class="flex justify-end">
{% if quiz.base_quiz %} {% if quiz.base_quiz %}
<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> <p class="text-gray-600 dark:text-white ">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 %} {% endif %}
</div> </div>
<div class="flex justify-end"> <div class="flex justify-end">
{% if quiz.base_quiz %} {% if quiz.base_quiz %}
<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> <p class="text-gray-600 dark:text-white">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 %} {% endif %}
</div> </div>
<div class="flex flex-col max-w-full"> <!-- Links müssen wieder ergänzt werden!
Credits: {% if questions %}
{% if quiz.credits %} <div class="flex flex-col sm:flex-row gap-4 items-start sm:items-center my-6">
<p class="text-gray-600 mb-2 w-full break-words">
<span class="font-bold break-words">{{ quiz.credits }}</span>
</p>
{% endif %} <a href=""
target="_blank"
class=" inline-flex items-center gap-2 px-2 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 text-white shadow-md transition">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4v16m8-8H4" />
</svg>
als Aufgabenblatt herunterladen (PDF)
</a>
<a href=""
target="_blank"
class="inline-flex items-center gap-2 px-2 py-2 rounded-lg bg-green-600 hover:bg-green-700 text-white shadow-md transition">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4v16m8-8H4" />
</svg>
Lösungen herunterladen (PDF)
</a>
</div> </div>
{% endif %}
-->
<div class="max-w-full p-4 mt-8 rounded-2xl bg-gray-100 dark:bg-transparent shadow-sm">
<div class="flex flex-col max-w-full">
<p class="text-lg font-bold text-gray-800 dark:text-white dark:bg-transparent mb-3">
Credits
</p>
{% if quiz.credits %}
<p class="text-blue-500 mb-2 w-full break-words dark:text-gray-400 ">
<span class="break-words italic">{{ quiz.credits }}</span>
<span class="font-normal break-words text-gray-600 dark:text-white">(Credits für das Quiz)</span>
</p>
{% else %}
<p class="text-gray-500 dark:text-gray-400 ">Keine Credits für das Quizformular vorhanden.</p>
{% endif %}
</div>
<div class="flex flex-col max-w-full"> <div class="flex flex-col max-w-full">
<ul class="space-y-2"> <ul class="space-y-2">
{% regroup questions by credits as grouped_credits %}
{% if grouped_credits|length == 1 and not grouped_credits.0.grouper %}
<li class="text-gray-500 dark:text-gray-400 ">Keine zusätzlichen Credits für die einzelnen Fragen vorhanden.</li>
{% else %}
{% for question in questions %} {% for question in questions %}
{% if question.credits %} {% if question.credits %}
<li class="text-gray-600 mb-2 w-full break-words font-bold break-words"> <li class="text-blue-500 mb-2 w-full break-words break-words dark:text-gray-400 ">
{{ question.credits }} <span class="italic">{{ question.credits }}</span>
<span class="font-medium text-gray-500 break-words">(Frage: {{ forloop.counter }})</span> <span class="font-normal break-words text-gray-600 dark:text-white">(Frage: {{ forloop.counter }})</span>
</li> </li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endif %}
</ul> </ul>
</div> </div>
</div>
<div class="max-w-full p-4 mt-8 rounded-2xl bg-gray-100 dark:bg-transparent shadow-sm">
<p class="text-lg font-bold text-gray-800 dark:text-white dark:bg-transparent mb-3">
Quiz-Statistik
</p>
<div class="space-y-2 text-gray-700 dark:text-white ">
<p>
<span class="font-semibold">{{ quiz.quiz_played_Lernmodus }}</span>
mal im Lernmodus gespielt
</p>
<p>
<span class="font-semibold">{{ quiz.quiz_played_Moderiermodus }}</span>
mal im Moderiermodus gespielt
<span class="text-sm text-gray-500 dark:text-gray-400">
(insg. {{ quiz.quiz_played_Moderiermodus_players }} Spieler)
</span>
</p>
<p class="text-xs opacity-50">
* zählt nur, wenn das Quiz vollständig beendet wird
</p>
</div>
</div>
</div> </div>

View File

@@ -1,8 +1,13 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
<div class="input-group border-blue-600 border-2 rounded-xl shadow-md mt-12">
<form method="post" enctype="multipart/form-data">
<div class="container mx-auto px-4 py-4">
<div class="max-w-lg field-group mx-auto bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent dark:text-white">
<form method="post" enctype="multipart/form-data" class="dark-form space-y-4">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} {{ form.as_p }}
@@ -15,7 +20,7 @@
{% endif %} {% endif %}
<div> <div>
<label class="font-bold" for="quiz_image">Bild:</label> <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"> <input class="max-w-full bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400 dark:bg-transparent" type="file" name="quiz_image" id="quiz_image">
</div> </div>
<div class="text-center text-gray-600 text-sm dislay-flex align-bottom"> <div class="text-center text-gray-600 text-sm dislay-flex align-bottom">
@@ -34,4 +39,5 @@
</div> </div>
</form> </form>
</div> </div>
</div>
{% endblock %} {% endblock %}

View File

@@ -12,14 +12,14 @@
{% block head %} {% block head %}
<div class="container mx-auto px-4 py-4 flex justify-between items-center"> <div class="container mx-auto px-4 py-4 flex justify-between items-center">
<button id="filterButton" class="p-2 hover:bg-gray-100 rounded-lg transition-colors duration-200 flex items-center gap-2 text-gray-700"> <button id="filterButton" class="dark:text-white dark:hover:bg-[#2a2f3a] p-2 hover:bg-gray-100 rounded-lg transition-colors duration-200 flex items-center gap-2 text-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg> </svg>
<span>Filter</span> <span>Filter</span>
</button> </button>
<div class="flex gap-2"> <div class="flex gap-2">
<a href="{% url link %}" class="inline-flex items-center px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors duration-200"> <a href="{% url link %}" class="dark:bg-transparent dark:text-white dark:hover:bg-[#2a2f3a] inline-flex items-center px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors duration-200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg> </svg>
@@ -35,13 +35,13 @@
</div> </div>
<div id="filterPanel" class="container mx-auto px-4 {% if request.GET.filter == '1' %}block{% else %}hidden{% endif %}"> <div id="filterPanel" class="container mx-auto px-4 {% if request.GET.filter == '1' %}block{% else %}hidden{% endif %}">
<div class="bg-white rounded-xl shadow-lg p-6 border border-gray-200"> <div class="bg-white rounded-xl shadow-lg p-6 border border-gray-200 dark:bg-transparent dark:text-white ">
<form action="{% url link %}" method="get" class="grid grid-cols-1 md:grid-cols-3 gap-6"> <form action="{% url link %}" method="get" class="grid grid-cols-1 md:grid-cols-3 gap-6">
<input type="hidden" name="filter" id="filter-input"> <input type="hidden" name="filter" id="filter-input">
<div class="space-y-2"> <div class="space-y-2">
<div class="flex gap-2"> <div class="flex gap-2">
<input type="checkbox" name="use_min_amount_questions" id="use_min_amount_questions" {% if form.use_min_amount_questions.value %}checked{% endif %}> <input type="checkbox" name="use_min_amount_questions" id="use_min_amount_questions" {% if form.use_min_amount_questions.value %}checked{% endif %}>
<label class="block text-sm font-medium text-gray-700">minimale Anzahl an Fragen</label> <label class="block text-sm font-medium text-gray-700 dark:text-white">minimale Anzahl an Fragen</label>
</div> </div>
<input <input
type="range" type="range"
@@ -51,22 +51,22 @@
max="100" max="100"
step="1" step="1"
value="{{ form.min_amount_questions.value|default_if_none:0 }}" value="{{ form.min_amount_questions.value|default_if_none:0 }}"
class="w-full accent-blue-600" class="w-full accent-blue-600 dark:text-white"
oninput="document.getElementById('min_amount_value').textContent = this.value + ' Frage(n)'" oninput="document.getElementById('min_amount_value').textContent = this.value + ' Frage(n)'"
{% if not form.use_min_amount_questions.value %}disabled{% endif %} {% if not form.use_min_amount_questions.value %}disabled{% endif %}
> >
<span id="min_amount_value" class="text-sm font-semibold text-gray-800"> <span id="min_amount_value" class="text-sm font-semibold text-gray-800 dark:text-white">
{{ form.min_amount_questions.value|default_if_none:0 }} Frage(n) {{ form.min_amount_questions.value|default_if_none:0 }} Frage(n)
</span> </span>
<br> <br>
<span class="text-xs text-gray-600"> Quizze anderer User werden erst ab min. 1 Frage angezeigt.</span> <span class="text-xs text-gray-600 dark:text-white"> Quizze anderer User werden erst ab min. 1 Frage angezeigt.</span>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<div class="flex gap-2"> <div class="flex gap-2">
<input type="checkbox" name="use_max_amount_questions" id="use_max_amount_questions" {% if form.use_max_amount_questions.value %}checked{% endif %}> <input type="checkbox" name="use_max_amount_questions" id="use_max_amount_questions" {% if form.use_max_amount_questions.value %}checked{% endif %}>
<label class="block text-sm font-medium text-gray-700">maximale Anzahl an Fragen</label> <label class="block text-sm font-medium text-gray-700 dark:text-white">maximale Anzahl an Fragen</label>
</div> </div>
<input <input
type="range" type="range"
@@ -81,22 +81,22 @@
{% if not form.use_max_amount_questions.value %}disabled{% endif %} {% if not form.use_max_amount_questions.value %}disabled{% endif %}
> >
<span id="max_amount_value" class="text-sm font-semibold text-gray-800"> <span id="max_amount_value" class="text-sm font-semibold text-gray-800 dark:text-white">
{{ form.max_amount_questions.value|default_if_none:100 }} Frage(n) {{ form.max_amount_questions.value|default_if_none:100 }} Frage(n)
</span> </span>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">veröffentlicht von User</label> <label class="block text-sm font-medium text-gray-700 dark:text-white">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" value="{{ form.user.value|default_if_none:'' }}"> <input type="text" name="user" class="border-2 border-gray-300 rounded-lg p-2 w-full dark:text-white" id="id_user" value="{{ form.user.value|default_if_none:'' }}">
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">Kategorie</label> <label class="block text-sm font-medium text-gray-700 dark:text-white">Kategorie</label>
{{ form.categories }} {{ form.categories }}
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">Suche</label> <label class="block text-sm font-medium text-gray-700 dark:text-white">Suche</label>
{{ form.search }} {{ form.search }}
</div> </div>

View File

@@ -18,20 +18,28 @@
{% endblock %} {% endblock %}
<!-- Alle --> <!-- Alle -->
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6"> <div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6 dark:text-white">
{% if user.is_authenticated %}
<div class="flex justify-end text-sm mb-2"> <div class="flex justify-end text-sm mb-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 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-6 text-blue-600">
<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" /> <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> </svg>
<div class="gap-2 mx-2"> <div class="gap-2 mx-2">
{% if user.is_authenticated %}
Anzahl aller Ergebnisse: Anzahl aller Ergebnisse:
<span class="font-bold"> {{ total_all_found }}</span> <span class="font-bold"> {{ total_all_found }}</span>
</div> </div>
(verschiedene: (verschiedene:
<span class="font-bold ml-1"> {{ total_all }}</span>) </div> <span class="font-bold ml-1"> {{ total_all }}</span>)
{% else %}
Anzahl aller Ergebnisse:
<span class="font-bold"> {{ total_all_found }}</span>
</div>
{% endif %}
</div>
{% if user.is_authenticated %}
<div class="flex flex-wrap justify-end gap-4 font-bold text-blue-600 whitespace-nowrap text-sm mb-2 md:mb-0"> <div class="flex flex-wrap justify-end gap-4 font-bold text-blue-600 whitespace-nowrap text-sm mb-2 md:mb-0">
<p class="font-black underline">Alle ({{ total_all }})</p> <p class="font-black underline">Alle ({{ total_all }})</p>
<a href="{{ url_my }}?{{ request.GET.urlencode }}">Meine Quizze ({{ total_my }})</a> <a href="{{ url_my }}?{{ request.GET.urlencode }}">Meine Quizze ({{ total_my }})</a>
@@ -40,7 +48,7 @@
{% endif %} {% endif %}
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<h2 id="all-quizzes" class="text-2xl font-bold text-gray-900">Alle</h2> <h2 id="all-quizzes" class="text-2xl font-bold text-gray-900 dark:text-white">Alle</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div> <div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div> </div>
@@ -52,7 +60,7 @@
<div class="container mx-auto px-4 sm:px-6 lg:px-8"> <div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 auto-rows-fr"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 auto-rows-fr">
{% for quiz in all_quizzes %} {% for quiz in all_quizzes %}
<article class="relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col"> <article class="relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden flex flex-col">
<!-- Image Section (Top) --> <!-- Image Section (Top) -->
<div class="h-48 w-full relative flex-shrink-0"> <div class="h-48 w-full relative flex-shrink-0">
{% if quiz.image %} {% if quiz.image %}
@@ -70,17 +78,17 @@
{% endif %} {% endif %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% if quiz.id not in favorite_quiz_ids %} {% if quiz.id not in favorite_quiz_ids %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg"> <div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg dark:bg-[#2a2f3a]">
<a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}"> <a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-black size-6"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-black size-6 dark:text-white">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
</svg> </svg>
</a> </a>
</div> </div>
{% else %} {% else %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg"> <div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg dark:bg-[#2a2f3a]">
<a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}"> <a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-yellow-400 size-6"> <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-yellow-400 size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
@@ -89,16 +97,27 @@
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
<div class="dark:bg-[#2a2f3a] max-w-6/10 h-max-12 truncate absolute top-4 left-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg ">
<div class="flex items-center gap-2">
<span class="rounded-full w-4 h-4 flex-shrink-0" style="background-color: {{ quiz.category.color }};"></span>
<a class=" text-gray-800 truncate text-sm dark:text-white" title="{{ quiz.category }}" href="{% url 'library:overview_quiz' %}?filter=1&categories={{ quiz.category.id }}">{{ quiz.category }}</a>
</div>
</div>
</div> </div>
<!-- Content Section (Bottom) --> <!-- Content Section (Bottom) -->
<div class="flex-grow bg-white p-4 flex flex-col gap-3"> <div class="flex-grow bg-white p-4 flex flex-col gap-3 dark:bg-[#2a2f3a] dark:text-white" >
<!-- Title --> <!-- Title -->
<h2 class="text-xl font-bold text-gray-900 break-words mb-2"> <h2 class="text-xl font-bold text-gray-900 break-words mb-2">
<a href="{% url 'library:detail_quiz' quiz.id %}" class="hover:text-blue-600 transition-colors">{{ quiz.name }}</a> <a href="{% url 'library:detail_quiz' quiz.id %}" class="hover:text-blue-600 transition-colors dark:text-white">{{ quiz.name }}</a>
</h2> </h2>
<!-- Description --> <!-- Description -->
<p class="text-sm text-gray-700 break-words mb-3">{{ quiz.description }}</p> <p class="text-sm text-gray-700 break-words mb-3 dark:text-white">{{ quiz.description }}</p>
<!-- Quiz Info Grid --> <!-- Quiz Info Grid -->
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm"> <div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
@@ -107,7 +126,7 @@
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg> </svg>
<span class="text-gray-800">{{ quiz.difficulty }}</span> <span class="text-gray-800 dark:text-white">{{ quiz.difficulty }}</span>
</div> </div>
<!-- Questions Count --> <!-- Questions Count -->
@@ -115,7 +134,7 @@
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<span class="text-gray-800">{{ quiz.questions.count }} Fragen</span> <span class="text-gray-800 dark:text-white">{{ quiz.questions.count }} Fragen</span>
</div> </div>
<!-- Status --> <!-- Status -->
@@ -123,11 +142,14 @@
<svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<span class="text-gray-800">{{ quiz.status }}</span> <span class="text-gray-800 dark:text-white">{{ quiz.status }}</span>
</div> </div>
<!-- Rating --> <!-- Rating -->
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
{% if quiz.rating_count > 0 %} {% if quiz.rating_count > 0 %}
@@ -138,9 +160,9 @@
<span class="text-gray-300 text-base"></span> <span class="text-gray-300 text-base"></span>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span> <span class="text-gray-600 text-sm dark:text-white">({{ quiz.rating_count }})</span>
{% else %} {% else %}
<span class="text-gray-600 text-sm">Noch keine Bewertungen</span> <span class="text-gray-600 text-sm dark:text-white">Noch keine Bewertungen</span>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -165,13 +187,13 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="text-green-400 size-4"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="text-green-400 size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg> </svg>
<span class="text-gray-800 text-sm "><span class="font-medium">{{ quiz.published_at |naturaltime }}</span> <span class="text-sm">erst. veröffentlicht</span> </span> <span class="text-gray-800 text-sm dark:text-white"><span class="font-medium dark:text-white">{{ quiz.published_at |naturaltime }}</span> <span class="text-sm">erst. veröffentlicht</span> </span>
</div> </div>
{% endif %} {% endif %}
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="flex justify-between items-center gap-2 mt-3 border-t pt-3"> <div class="flex justify-between items-center gap-2 mt-3 border-t pt-3" >
{% if quiz.questions.count != 0 %} {% if quiz.questions.count != 0 %}
<a href="{% url 'play:create_game' quiz.id %}" <a href="{% url 'play:create_game' quiz.id %}"
class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 flex items-center gap-2"> class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 flex items-center gap-2">
@@ -186,12 +208,26 @@
{% endif %} {% endif %}
<div class="flex gap-1"> <div class="flex gap-1">
<a href="{% url 'library:detail_quiz' quiz.id %}" <a href="{% url 'library:detail_quiz' quiz.id %}"
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200"> class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700 ">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg> </svg>
</a> </a>
{% if quiz.user_id == request.user %}
<a href="{% url 'library:edit_quiz' quiz.id %}"
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</a>
<a href="{% url 'library:delete_quiz' quiz.id %}"
class="p-1.5 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</a>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@@ -211,7 +247,7 @@
</a> </a>
{% endif %} {% endif %}
<span class="px-3 py-1 text-gray-600">Seite {{ all_quizzes.number }} von {{ all_quizzes.paginator.num_pages }}</span> <span class="px-3 py-1 text-gray-600 dark:text-white">Seite {{ all_quizzes.number }} von {{ all_quizzes.paginator.num_pages }}</span>
{% if all_quizzes.has_next %} {% if all_quizzes.has_next %}
<a href="?all_page={{ all_quizzes.next_page_number }}&{{ querystring_all }}" <a href="?all_page={{ all_quizzes.next_page_number }}&{{ querystring_all }}"
@@ -226,7 +262,7 @@
</div> </div>
{% else %} {% else %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-4"> <div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-4">
<p class="text-gray-600 text-center">Keine öffentlichen Quiz gefunden.</p> <p class="text-gray-600 text-center dark:text-white">Keine Quiz gefunden.</p>
</div> </div>
{% endif %}<script> {% endif %}<script>

View File

@@ -22,14 +22,14 @@
{% if user.is_authenticated %} {% if user.is_authenticated %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6"> <div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6 dark:text-white">
<div class="flex justify-end text-sm mb-2"> <div class="flex justify-end text-sm mb-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 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-6 text-blue-600">
<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" /> <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> </svg>
<div class="gap-2 mx-2"> <div class="gap-2 mx-2 ">
Anzahl aller Ergebnisse: Anzahl aller Ergebnisse:
<span class="font-bold"> {{ total_all_found }}</span> <span class="font-bold"> {{ total_all_found }}</span>
</div> </div>
@@ -46,7 +46,7 @@
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<h2 id="my-quizzes" class="text-2xl font-bold text-gray-900">Meine Favoriten</h2> <h2 id="my-quizzes" class="text-2xl font-bold text-gray-900 dark:text-white">Meine Favoriten</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div> <div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div> </div>
</div> </div>
@@ -54,9 +54,9 @@
<div class="container mx-auto px-4 sm:px-6 lg:px-8"> <div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 auto-rows-fr"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 auto-rows-fr">
{% for quiz in favorite_quizzes %} {% for quiz in favorite_quizzes %}
<article class="relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col"> <article class="relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col ">
<!-- Image Section (Top) --> <!-- Image Section (Top) -->
<div class="h-48 w-full relative flex-shrink-0"> <div class="h-48 w-full relative flex-shrink-0 ">
{% if quiz.image %} {% if quiz.image %}
<div class="relative w-full h-full inset-0 bg-cover bg-center"> <div class="relative w-full h-full inset-0 bg-cover bg-center">
<!-- Hintergrundbild --> <!-- Hintergrundbild -->
@@ -72,17 +72,17 @@
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% if quiz.favorite == False %} {% if quiz.favorite == False %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg"> <div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg dark:bg-[#2a2f3a]">
<a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}"> <a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-black size-6"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-black size-6 dark:text-white">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
</svg> </svg>
</a> </a>
</div> </div>
{% else %} {% else %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg"> <div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg dark:bg-[#2a2f3a]">
<a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}"> <a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-yellow-400 size-6"> <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-yellow-400 size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
@@ -91,18 +91,30 @@
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
<div class="dark:bg-[#2a2f3a] max-w-6/10 h-max-12 truncate absolute top-4 left-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg ">
<div class="flex items-center gap-2">
<span class="rounded-full w-4 h-4 flex-shrink-0" style="background-color: {{ quiz.category.color }};"></span>
<a class=" text-gray-800 truncate text-sm dark:text-white" title="{{ quiz.category }}" href="{% url 'library:overview_quiz' %}?filter=1&categories={{ quiz.category.id }}">{{ quiz.category }}</a>
</div>
</div>
</div> </div>
<!-- Content Section (Bottom) --> <!-- Content Section (Bottom) -->
<div class="flex-grow bg-white p-4 flex flex-col gap-3"> <div class="flex-grow bg-white p-4 flex flex-col gap-3 dark:bg-[#2a2f3a] dark:text-white">
<!-- Title --> <!-- Title -->
<h2 class="text-xl font-bold text-gray-900 break-words mb-2"> <h2 class="text-xl font-bold text-gray-900 break-words mb-2">
<a href="{% url 'library:detail_quiz' quiz.id %}" class="hover:text-blue-600 transition-colors">{{ quiz.name }}</a> <a href="{% url 'library:detail_quiz' quiz.id %}" class="hover:text-blue-600 transition-colors dark:text-white">{{ quiz.name }}</a>
</h2> </h2>
<!-- Description --> <!-- Description -->
<p class="text-sm text-gray-700 break-words mb-3">{{ quiz.description }}</p> <p class="text-sm text-gray-700 break-words mb-3 dark:text-white">{{ quiz.description }}</p>
<!-- Quiz Info Grid --> <!-- Quiz Info Grid -->
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm"> <div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
@@ -111,7 +123,7 @@
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg> </svg>
<span class="text-gray-800">{{ quiz.difficulty }}</span> <span class="text-gray-800 dark:text-white">{{ quiz.difficulty }}</span>
</div> </div>
<!-- Questions Count --> <!-- Questions Count -->
@@ -119,7 +131,7 @@
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<span class="text-gray-800">{{ quiz.questions.count }} Fragen</span> <span class="text-gray-800 dark:text-white">{{ quiz.questions.count }} Fragen</span>
</div> </div>
<!-- Status --> <!-- Status -->
@@ -127,7 +139,7 @@
<svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<span class="text-gray-800">{{ quiz.status }}</span> <span class="text-gray-800 dark:text-white">{{ quiz.status }}</span>
</div> </div>
<!-- Rating --> <!-- Rating -->
@@ -140,9 +152,9 @@
<span class="text-gray-300 text-base"></span> <span class="text-gray-300 text-base"></span>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span> <span class="text-gray-600 text-sm dark:text-white">({{ quiz.rating_count }})</span>
{% else %} {% else %}
<span class="text-gray-600 text-sm">Noch keine Bewertungen</span> <span class="text-gray-600 text-sm dark:text-white">Noch keine Bewertungen</span>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -169,7 +181,7 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="text-green-400 size-4"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="text-green-400 size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg> </svg>
<span class="text-gray-800 text-sm "><span class="font-medium">{{ quiz.published_at |naturaltime }}</span> <span class="text-sm">erst. veröffentlicht</span> </span> <span class="text-gray-800 text-sm dark:text-white "><span class="font-medium dark:text-white">{{ quiz.published_at |naturaltime }}</span> <span class="text-sm">erst. veröffentlicht</span> </span>
</div> </div>
{% endif %} {% endif %}
@@ -190,7 +202,7 @@
<div class="flex gap-1"> <div class="flex gap-1">
<a href="{% url 'library:detail_quiz' quiz.id %}" <a href="{% url 'library:detail_quiz' quiz.id %}"
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200"> class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
@@ -198,13 +210,13 @@
</a> </a>
{% if quiz.user_id == request.user %} {% if quiz.user_id == request.user %}
<a href="{% url 'library:edit_quiz' quiz.id %}" <a href="{% url 'library:edit_quiz' quiz.id %}"
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200"> class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg> </svg>
</a> </a>
<a href="{% url 'library:delete_quiz' quiz.id %}" <a href="{% url 'library:delete_quiz' quiz.id %}"
class="p-1.5 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors duration-200"> class="p-1.5 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg> </svg>
@@ -218,9 +230,9 @@
</div> </div>
<!-- Pagination für Meine Quizze --> <!-- Pagination für favorite Quizze -->
{% if favorite_quizzes.paginator.num_pages > 1 %} {% if favorite_quizzes.paginator.num_pages > 1 %}
<div class="flex justify-center space-x-2 mt-6 mb-8"> <div class="flex justify-center space-x-2 mt-6 mb-8 ">
{% if favorite_quizzes.has_previous %} {% if favorite_quizzes.has_previous %}
<a href="?favorite_page={{ favorite.previous_page_number }}&{{ querystring_favorite }}" <a href="?favorite_page={{ favorite.previous_page_number }}&{{ querystring_favorite }}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors"> class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
@@ -230,7 +242,7 @@
</a> </a>
{% endif %} {% endif %}
<span class="px-3 py-1 text-gray-600">Seite {{ favorite_quizzes.number }} von {{ favorite_quizzes.paginator.num_pages }}</span> <span class="dark:text-white px-3 py-1 text-gray-600">Seite {{ favorite_quizzes.number }} von {{ favorite_quizzes.paginator.num_pages }}</span>
{% if favorite_quizzes.has_next %} {% if favorite_quizzes.has_next %}
<a href="?favorite_page={{ favorite_quizzes.next_page_number }}&{{ querystring_favorite }}" <a href="?favorite_page={{ favorite_quizzes.next_page_number }}&{{ querystring_favorite }}"
@@ -245,7 +257,7 @@
</div> </div>
{% else %} {% else %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-4"> <div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-4">
<p class="text-gray-600 text-center">Keine favorisierten Quiz gefunden.</p> <p class="text-gray-600 text-center dark:text-white">Keine favorisierten Quiz gefunden.</p>
</div> </div>
{% endif %}<script> {% endif %}<script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {

View File

@@ -20,7 +20,7 @@
{% if user.is_authenticated %} {% if user.is_authenticated %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6"> <div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6 dark:text-white">
<div class="flex justify-end text-sm mb-2"> <div class="flex justify-end text-sm mb-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 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-6 text-blue-600">
@@ -41,7 +41,7 @@
<a href="{{ url_favorite }}?{{ request.GET.urlencode }}">Meine Favoriten ({{ total_favorite }})</a> <a href="{{ url_favorite }}?{{ request.GET.urlencode }}">Meine Favoriten ({{ total_favorite }})</a>
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<h2 id="my-quizzes" class="text-2xl font-bold text-gray-900">Meine Quizze</h2> <h2 id="my-quizzes" class="text-2xl font-bold text-gray-900 dark:text-white">Meine Quizze</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div> <div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div> </div>
</div> </div>
@@ -69,17 +69,19 @@
{% endif %} {% endif %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% if quiz.id not in favorite_quiz_ids %} {% if quiz.id not in favorite_quiz_ids %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg"> <div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg dark:bg-[#2a2f3a]">
<a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}"> <a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-black size-6"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-black size-6 dark:text-white">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
</svg> </svg>
</a> </a>
</div> </div>
{% else %} {% else %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg"> <div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg dark:bg-[#2a2f3a]">
<a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}"> <a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-yellow-400 size-6"> <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-yellow-400 size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
@@ -88,18 +90,28 @@
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
<div class="dark:bg-[#2a2f3a] max-w-6/10 h-max-12 truncate absolute top-4 left-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg ">
<div class="flex items-center gap-2">
<span class="rounded-full w-4 h-4 flex-shrink-0" style="background-color: {{ quiz.category.color }};"></span>
<a class=" text-gray-800 truncate text-sm dark:text-white" title="{{ quiz.category }}" href="{% url 'library:overview_quiz' %}?filter=1&categories={{ quiz.category.id }}">{{ quiz.category }}</a>
</div>
</div>
</div> </div>
<!-- Content Section (Bottom) --> <!-- Content Section (Bottom) -->
<div class="flex-grow bg-white p-4 flex flex-col gap-3"> <div class="flex-grow bg-white p-4 flex flex-col gap-3 dark:bg-[#2a2f3a] dark:text-white">
<!-- Title --> <!-- Title -->
<h2 class="text-xl font-bold text-gray-900 break-words mb-2"> <h2 class="text-xl font-bold text-gray-900 break-words mb-2">
<a href="{% url 'library:detail_quiz' quiz.id %}" class="hover:text-blue-600 transition-colors">{{ quiz.name }}</a> <a href="{% url 'library:detail_quiz' quiz.id %}" class="hover:text-blue-600 transition-colors dark:text-white">{{ quiz.name }}</a>
</h2> </h2>
<!-- Description --> <!-- Description -->
<p class="text-sm text-gray-700 break-words mb-3">{{ quiz.description }}</p> <p class="text-sm text-gray-700 break-words mb-3 dark:text-white">{{ quiz.description }}</p>
<!-- Quiz Info Grid --> <!-- Quiz Info Grid -->
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm"> <div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
@@ -108,7 +120,7 @@
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg> </svg>
<span class="text-gray-800">{{ quiz.difficulty }}</span> <span class="text-gray-800 dark:text-white">{{ quiz.difficulty }}</span>
</div> </div>
<!-- Questions Count --> <!-- Questions Count -->
@@ -116,7 +128,7 @@
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<span class="text-gray-800">{{ quiz.questions.count }} Fragen</span> <span class="text-gray-800 dark:text-white">{{ quiz.questions.count }} Fragen</span>
</div> </div>
<!-- Status --> <!-- Status -->
@@ -124,7 +136,7 @@
<svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<span class="text-gray-800">{{ quiz.status }}</span> <span class="text-gray-800 dark:text-white">{{ quiz.status }}</span>
</div> </div>
<!-- Rating --> <!-- Rating -->
@@ -137,9 +149,9 @@
<span class="text-gray-300 text-base"></span> <span class="text-gray-300 text-base"></span>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span> <span class="text-gray-600 text-sm dark:text-white">({{ quiz.rating_count }})</span>
{% else %} {% else %}
<span class="text-gray-600 text-sm">Noch keine Bewertungen</span> <span class="text-gray-600 text-sm dark:text-white">Noch keine Bewertungen</span>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -168,7 +180,7 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="text-green-400 size-4"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="text-green-400 size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg> </svg>
<span class="text-gray-800 text-sm "><span class="font-medium">{{ quiz.published_at |naturaltime }}</span> <span class="text-sm">erst. veröffentlicht</span> </span> <span class="text-gray-800 text-sm dark:text-white"><span class="font-medium dark:text-white">{{ quiz.published_at |naturaltime }}</span> <span class="text-sm">erst. veröffentlicht</span> </span>
</div> </div>
{% endif %} {% endif %}
@@ -188,7 +200,7 @@
{% endif %} {% endif %}
<div class="flex gap-1"> <div class="flex gap-1">
<a href="{% url 'library:detail_quiz' quiz.id %}" <a href="{% url 'library:detail_quiz' quiz.id %}"
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200"> class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
@@ -196,13 +208,13 @@
</a> </a>
{% if quiz.user_id == request.user %} {% if quiz.user_id == request.user %}
<a href="{% url 'library:edit_quiz' quiz.id %}" <a href="{% url 'library:edit_quiz' quiz.id %}"
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200"> class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg> </svg>
</a> </a>
<a href="{% url 'library:delete_quiz' quiz.id %}" <a href="{% url 'library:delete_quiz' quiz.id %}"
class="p-1.5 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors duration-200"> class="p-1.5 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg> </svg>
@@ -228,7 +240,7 @@
</a> </a>
{% endif %} {% endif %}
<span class="px-3 py-1 text-gray-600">Seite {{ my_quizzes.number }} von {{ my_quizzes.paginator.num_pages }}</span> <span class="px-3 py-1 text-gray-600 dark:text-white">Seite {{ my_quizzes.number }} von {{ my_quizzes.paginator.num_pages }}</span>
{% if my_quizzes.has_next %} {% if my_quizzes.has_next %}
<a href="?my_page={{ my_quizzes.next_page_number }}&{{ querystring_my }}" <a href="?my_page={{ my_quizzes.next_page_number }}&{{ querystring_my }}"
@@ -243,7 +255,7 @@
</div> </div>
{% else %} {% else %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-4"> <div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-4">
<p class="text-gray-600 text-center">Keine eigenen Quiz gefunden.</p> <p class="text-gray-600 text-center dark:text-white">Keine eigenen Quiz gefunden.</p>
</div> </div>
{% endif %}<script> {% endif %}<script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {

View File

@@ -0,0 +1,150 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mx-auto px-4">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold dark:text-white">{% if question %}Frage bearbeiten{% else %}Neue Schätzfrage{% endif %}</h1>
<a href="{% url 'library:detail_quiz' quiz.id %}"
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
Zurück zum Quiz
</a>
</div>
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent dark:text-white">
<form method="post" class="space-y-6" enctype="multipart/form-data">
{% csrf_token %}
<!-- Question Text -->
<div>
<label for="question" class="block text-sm font-medium text-gray-700 dark:text-white">Frage</label>
<div class="mt-1">
<textarea name="question" id="question" rows="4"
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
required>{{ question.data.question }}</textarea>
</div>
</div>
{% if image %}
<img src="{{ image.image.url }}" style="max-width: 100px;" 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>
<input class="max-w-full bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400 dark:bg-transparent" type="file" name="question_image" id="question_image">
</div>
<!-- Eingabe -->
{% if question and question.data.options %}
{% for option in question.data.options %}
<div>
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Richtige Antwort</label>
<div class="flex space-x-4">
<input name="correct_answer" type="number" value="{{ question.data.correct_answer|default:'' }}"
class="p-3 dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></input>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Minimale Zahlenauswahl</label>
<div class="flex space-x-4">
<input name="min" type="number" value="{{ question.data.min|default:'' }}"
class="p-3 dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></input>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Maximale Zahlenauswahl</label>
<div class="flex space-x-4">
<input name="max" type="number" value="{{ question.data.max|default:'' }}"
class="p-3 dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></input>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Punkte ab Genauigkeit von ... (in %)</label>
<div class="flex space-x-4">
<input name="tolerance" type="number" value="{{ question.data.tolerance|default:'' }}" max="100" min="1" value="75"
class=" p-3 dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></input>
</div>
</div>
{% endfor %}
{% else %}
<div>
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Richtige Antwort</label>
<div class="flex space-x-4">
<input name="correct_answer" type="number"
class="p-3 dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></input>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Minimale Zahlenauswahl</label>
<div class="flex space-x-4">
<input name="min" type="number" value="0"
class="p-3 dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></input>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Maximale Zahlenauswahl</label>
<div class="flex space-x-4">
<input name="max" type="number"
class="p-3 dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></input>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Punkte ab Genauigkeit von ... (in %)</label>
<div class="flex space-x-4">
<input name="tolerance" type="number" max="100" min="1" value="75"
class=" p-3 dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></input>
</div>
</div>
{% endif %}
<div class="p-4">
<label class="font-bold" for="time_per_question">Zeit pro Frage:</label>
<select name="time_per_question" id="time_per_question" class="dark:text-white dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="10" {% if standard_time_per_question == "10" or time_per_question == "10" %}selected{% endif %}>10 Sekunden</option>
<option value="20" {% if standard_time_per_question == "20" or time_per_question == "20" %}selected{% endif %}>20 Sekunden</option>
<option value="30" {% if standard_time_per_question == "30" or time_per_question == "30" %}selected{% endif %}>30 Sekunden</option>
<option value="45" {% if standard_time_per_question == "45" or time_per_question == "45" %}selected{% endif %}>45 Sekunden</option>
<option value="60" {% if standard_time_per_question == "60" or time_per_question == "60" %}selected{% endif %}>1 Minute</option>
<option value="120" {% if standard_time_per_question == "120" or time_per_question == "120" %}selected{% endif %}>2 Minuten</option>
<option value="300" {% if standard_time_per_question == "300" or time_per_question == "300" %}selected{% endif %}>5 Minuten</option>
</select>
</div>
<label for="credits" class="block text-sm font-medium text-gray-700 dark:text-white">Credits</label>
<div class="mt-1">
<textarea name="credits" rows="4"
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
>{{ credits }}</textarea>
</div>
<div class="flex justify-end space-x-3 mt-6">
<a href="{% url 'library:detail_quiz' quiz.id %}"
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">
Abbrechen
</a>
<button type="submit"
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Speichern
</button>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -3,23 +3,23 @@
{% block content %} {% block content %}
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">{% if question %}Frage bearbeiten{% else %}Neue Eingabe Frage{% endif %}</h1> <h1 class="text-2xl font-bold dark:text-white">{% if question %}Frage bearbeiten{% else %}Neue Eingabe Frage{% endif %}</h1>
<a href="{% url 'library:detail_quiz' quiz.id %}" <a href="{% url 'library:detail_quiz' quiz.id %}"
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors"> class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
Zurück zum Quiz Zurück zum Quiz
</a> </a>
</div> </div>
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md"> <div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent dark:text-white">
<form method="post" class="space-y-6" enctype="multipart/form-data"> <form method="post" class="space-y-6" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<!-- Question Text --> <!-- Question Text -->
<div> <div>
<label for="question" class="block text-sm font-medium text-gray-700">Frage</label> <label for="question" class="block text-sm font-medium text-gray-700 dark:text-white">Frage</label>
<div class="mt-1"> <div class="mt-1">
<textarea name="question" id="question" rows="4" <textarea name="question" id="question" rows="4"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
required>{{ question.data.question }}</textarea> required>{{ question.data.question }}</textarea>
</div> </div>
</div> </div>
@@ -33,17 +33,17 @@
{% endif %} {% endif %}
<div> <div>
<label class="font-bold" for="question_image">Bild:</label> <label class="font-bold" for="question_image">Bild:</label>
<input class="bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400" type="file" name="question_image" id="question_image"> <input class="max-w-full bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400 dark:bg-transparent" type="file" name="question_image" id="question_image">
</div> </div>
<!-- Eingabe --> <!-- Eingabe -->
{% if question and question.data.options %} {% if question and question.data.options %}
{% for option in question.data.options %} {% for option in question.data.options %}
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-2">Richtige Antwort</label> <label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Richtige Antwort</label>
<div class="flex space-x-4"> <div class="flex space-x-4">
<textarea name="correct_answer" rows="2" <textarea name="correct_answer" rows="2"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md" class="dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required>{{ option.value }}</textarea> required>{{ option.value }}</textarea>
</div> </div>
@@ -52,10 +52,10 @@
{% endfor %} {% endfor %}
{% else %} {% else %}
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-2">Richtige Antwort</label> <label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Richtige Antwort</label>
<div class="flex space-x-4"> <div class="flex space-x-4">
<textarea name="correct_answer" rows="2" <textarea name="correct_answer" rows="2"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md" class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></textarea> required></textarea>
</div> </div>
@@ -66,7 +66,7 @@
<div class="p-4"> <div class="p-4">
<label class="font-bold" for="time_per_question">Zeit pro Frage:</label> <label class="font-bold" for="time_per_question">Zeit pro Frage:</label>
<select name="time_per_question" id="time_per_question"> <select name="time_per_question" id="time_per_question" class="dark:text-white dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="10" {% if standard_time_per_question == "10" or time_per_question == "10" %}selected{% endif %}>10 Sekunden</option> <option value="10" {% if standard_time_per_question == "10" or time_per_question == "10" %}selected{% endif %}>10 Sekunden</option>
<option value="20" {% if standard_time_per_question == "20" or time_per_question == "20" %}selected{% endif %}>20 Sekunden</option> <option value="20" {% if standard_time_per_question == "20" or time_per_question == "20" %}selected{% endif %}>20 Sekunden</option>
<option value="30" {% if standard_time_per_question == "30" or time_per_question == "30" %}selected{% endif %}>30 Sekunden</option> <option value="30" {% if standard_time_per_question == "30" or time_per_question == "30" %}selected{% endif %}>30 Sekunden</option>
@@ -76,10 +76,10 @@
<option value="300" {% if standard_time_per_question == "300" or time_per_question == "300" %}selected{% endif %}>5 Minuten</option> <option value="300" {% if standard_time_per_question == "300" or time_per_question == "300" %}selected{% endif %}>5 Minuten</option>
</select> </select>
</div> </div>
<label for="credits" class="block text-sm font-medium text-gray-700">Credits</label> <label for="credits" class="block text-sm font-medium text-gray-700 dark:text-white">Credits</label>
<div class="mt-1"> <div class="mt-1">
<textarea name="credits" rows="4" <textarea name="credits" rows="4"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
>{{ credits }}</textarea> >{{ credits }}</textarea>
</div> </div>

View File

@@ -0,0 +1,166 @@
{% extends 'base.html' %}
{% block content %}
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<div id="mapquestion-container">
</div>
<div class="container mx-auto px-4">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold dark:text-white">{% if question %}Frage bearbeiten{% else %}Neue Karten Frage{% endif %}</h1>
<a href="{% url 'library:detail_quiz' quiz.id %}"
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
Zurück zum Quiz
</a>
</div>
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent dark:text-white">
<form method="post" class="space-y-6" enctype="multipart/form-data">
{% csrf_token %}
<!-- Question Text -->
<div>
<label for="question" class="block text-sm font-medium text-gray-700 dark:text-white">Frage</label>
<div class="mt-1">
<textarea name="question" id="question" rows="4"
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
required>{{ question.data.question }}</textarea>
</div>
</div>
{% if image %}
<img src="{{ image.image.url }}" style="max-width: 100px;" 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>
<input class="max-w-full bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400 dark:bg-transparent" type="file" name="question_image" id="question_image">
</div>
<label for="map" class="block text-sm font-medium text-gray-700 dark:text-white">Ziel auswählen</label>
<div id="map"></div>
<label for="location" class="block text-sm font-medium text-gray-700 dark:text-white">Koordinaten</label>
<div class="mt-1">
<textarea id="location" name="location" rows="2" required
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md "
>{{ question.data.target_location }}</textarea>
</div>
<label for="tolerance_radius" class="block text-sm font-medium text-gray-700 dark:text-white">Radius (Meter) für volle Punktzahl</label>
<div class="mt-1">
<textarea id="tolerance_radius" name="tolerance_radius" rows="2" required
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md "
>{{ question.data.tolerance_radius }}</textarea>
</div>
<div class="p-4">
<label class="font-bold" for="time_per_question">Zeit pro Frage:</label>
<select name="time_per_question" id="time_per_question" class="dark:text-white dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="10" {% if standard_time_per_question == "10" or time_per_question == "10" %}selected{% endif %}>10 Sekunden</option>
<option value="20" {% if standard_time_per_question == "20" or time_per_question == "20" %}selected{% endif %}>20 Sekunden</option>
<option value="30" {% if standard_time_per_question == "30" or time_per_question == "30" %}selected{% endif %}>30 Sekunden</option>
<option value="45" {% if standard_time_per_question == "45" or time_per_question == "45" %}selected{% endif %}>45 Sekunden</option>
<option value="60" {% if standard_time_per_question == "60" or time_per_question == "60" %}selected{% endif %}>1 Minute</option>
<option value="120" {% if standard_time_per_question == "120" or time_per_question == "120" %}selected{% endif %}>2 Minuten</option>
<option value="300" {% if standard_time_per_question == "300" or time_per_question == "300" %}selected{% endif %}>5 Minuten</option>
</select>
</div>
<label for="credits" class="block text-sm font-medium text-gray-700 dark:text-white">Credits</label>
<div class="mt-1">
<textarea name="credits" rows="4"
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md "
>{{ credits }}</textarea>
</div>
<div class="flex justify-end space-x-3">
<a href="{% url 'library:detail_quiz' quiz.id %}"
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">
Abbrechen
</a>
<button type="submit"
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
{% if question %}Speichern{% else %}Erstellen{% endif %}
</button>
</div>
</form>
</div>
</div>
<script>
let marker = null;
let circle = null;
const locationInput = document.getElementById('location');
const radiusInput = document.getElementById('tolerance_radius');
const map = L.map('map').setView([51.72, 10.45], 3);
// Prüfe, ob Dark Mode aktiv ist
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
// Wähle das passende Karten-Design
const tileUrl = isDarkMode
? 'https://{s}.basemaps.cartocdn.com/rastertiles/dark_all/{z}/{x}/{y}.png'
: 'https://{s}.basemaps.cartocdn.com/rastertiles/light_all/{z}/{x}/{y}.png';
L.tileLayer(tileUrl, {
maxZoom: 19,
attribution: '© OpenStreetMap © CARTO'
}).addTo(map);
function updateCircle(latlng) {
const radius = parseInt(radiusInput.value) || 100;
if (circle) {
map.removeLayer(circle);
}
circle = L.circle(latlng, {
color: 'blue',
fillColor: '#87CEEB',
fillOpacity: 0.2,
radius: radius
}).addTo(map);
}
function updateInput(latlng) {
locationInput.value = `${latlng.lat.toFixed(6)},${latlng.lng.toFixed(6)}`;
if (marker) {
updateCircle(marker.getLatLng());
}
}
function placeOrMoveMarker(latlng) {
if (marker) {
marker.setLatLng(latlng);
} else {
marker = L.marker(latlng, { draggable: true }).addTo(map);
marker.on('dragend', () => {
updateInput(marker.getLatLng());
});
}
updateInput(latlng);
}
// Update circle when radius changes
radiusInput.addEventListener('input', () => {
if (marker) {
updateCircle(marker.getLatLng());
}
});
// Place marker at existing coordinates if available
const existingCoords = '{{ question.data.target_location }}';
if (existingCoords) {
const [lat, lng] = existingCoords.split(',').map(Number);
placeOrMoveMarker({ lat, lng });
map.setView([lat, lng], 6); // Zoom in to show the marker
}
map.on('click', e => placeOrMoveMarker(e.latlng));
</script>
{% endblock %}

View File

@@ -3,22 +3,22 @@
{% block content %} {% block content %}
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">{% if question %}Frage bearbeiten{% else %}Neue Multiple Choice Frage{% endif %}</h1> <h1 class="text-2xl font-bold dark:text-white">{% if question %}Frage bearbeiten{% else %}Neue Multiple Choice Frage{% endif %}</h1>
<a href="{% url 'library:detail_quiz' quiz.id %}" <a href="{% url 'library:detail_quiz' quiz.id %}"
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors"> class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
Zurück zum Quiz Zurück zum Quiz
</a> </a>
</div> </div>
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md"> <div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent dark:text-white">
<form method="post" class="space-y-6" enctype="multipart/form-data"> <form method="post" class="space-y-6" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<!-- Question Text --> <!-- Question Text -->
<div> <div>
<label for="question" class="block text-sm font-medium text-gray-700">Frage</label> <label for="question" class="block text-sm font-medium text-gray-700 dark:text-white">Frage</label>
<div class="mt-1"> <div class="mt-1">
<textarea name="question" id="question" rows="4" <textarea name="question" id="question" rows="4"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
required>{{ question.data.question|default:'' }}</textarea> required>{{ question.data.question|default:'' }}</textarea>
</div> </div>
</div> </div>
@@ -33,24 +33,24 @@
{% endif %} {% endif %}
<div> <div>
<label class="font-bold" for="question_image">Bild:</label> <label class="font-bold" for="question_image">Bild:</label>
<input value="image.url" class="bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400" type="file" name="question_image" id="question_image"> <input value="image.url" class="max-w-full bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400 dark:bg-transparent" type="file" name="question_image" id="question_image">
</div> </div>
<!-- Options --> <!-- Options -->
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-2">Antwortmöglichkeiten</label> <label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Antwortmöglichkeiten</label>
<div id="options-container" class="space-y-3"> <div id="options-container" class="space-y-3">
{% if question and question.data.options %} {% if question and question.data.options %}
{% for option in question.data.options %} {% for option in question.data.options|dictsort:"order" %}
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<textarea name="option_{{ forloop.counter }}" rows="2" <textarea name="option_{{ forloop.counter }}" rows="2"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md" class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required>{{ option.value }}</textarea> required>{{ option.value }}</textarea>
<label class="inline-flex items-center"> <label class="inline-flex items-center">
<input type="checkbox" name="correct_{{ forloop.counter }}" value="1" <input type="checkbox" name="correct_{{ forloop.counter }}" value="1"
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded" class=" focus:outline-none focus:ring-2 focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
{% if option.is_correct %}checked{% endif %}> {% if option.is_correct %}checked{% endif %}>
<span class="ml-2 text-sm text-gray-600">Richtig</span> <span class="ml-2 text-sm text-gray-600 dark:text-white dark:text-white">Richtig</span>
</label> </label>
<button type="button" onclick="removeOption(this)" <button type="button" onclick="removeOption(this)"
class="text-red-600 hover:text-red-800 transition-colors"> class="text-red-600 hover:text-red-800 transition-colors">
@@ -63,12 +63,12 @@
{% else %} {% else %}
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<textarea name="option_1" rows="2" <textarea name="option_1" rows="2"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md" class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></textarea> required></textarea>
<label class="inline-flex items-center"> <label class="inline-flex items-center">
<input type="checkbox" name="correct_1" value="1" <input type="checkbox" name="correct_1" value="1"
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"> class=" focus:outline-none focus:ring-2 focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded">
<span class="ml-2 text-sm text-gray-600">Richtig</span> <span class="ml-2 text-sm text-gray-600 dark:text-white">Richtig</span>
</label> </label>
<button type="button" onclick="removeOption(this)" <button type="button" onclick="removeOption(this)"
class="text-red-600 hover:text-red-800 transition-colors"> class="text-red-600 hover:text-red-800 transition-colors">
@@ -79,12 +79,12 @@
</div> </div>
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<textarea name="option_2" rows="2" <textarea name="option_2" rows="2"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md" class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></textarea> required></textarea>
<label class="inline-flex items-center"> <label class="inline-flex items-center">
<input type="checkbox" name="correct_2" value="1" <input type="checkbox" name="correct_2" value="1"
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"> class=" focus:outline-none focus:ring-2 focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded">
<span class="ml-2 text-sm text-gray-600">Richtig</span> <span class="ml-2 text-sm text-gray-600 dark:text-white">Richtig</span>
</label> </label>
<button type="button" onclick="removeOption(this)" <button type="button" onclick="removeOption(this)"
class="text-red-600 hover:text-red-800 transition-colors"> class="text-red-600 hover:text-red-800 transition-colors">
@@ -95,12 +95,12 @@
</div> </div>
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<textarea name="option_3" rows="2" <textarea name="option_3" rows="2"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md" class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></textarea> required></textarea>
<label class="inline-flex items-center"> <label class="inline-flex items-center">
<input type="checkbox" name="correct_3" value="1" <input type="checkbox" name="correct_3" value="1"
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"> class=" focus:outline-none focus:ring-2 focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded">
<span class="ml-2 text-sm text-gray-600">Richtig</span> <span class="ml-2 text-sm text-gray-600 dark:text-white">Richtig</span>
</label> </label>
<button type="button" onclick="removeOption(this)" <button type="button" onclick="removeOption(this)"
class="text-red-600 hover:text-red-800 transition-colors"> class="text-red-600 hover:text-red-800 transition-colors">
@@ -111,12 +111,12 @@
</div> </div>
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<textarea name="option_4" rows="2" <textarea name="option_4" rows="2"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md" class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></textarea> required></textarea>
<label class="inline-flex items-center"> <label class="inline-flex items-center">
<input type="checkbox" name="correct_4" value="1" <input type="checkbox" name="correct_4" value="1"
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"> class=" focus:outline-none focus:ring-2 focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded">
<span class="ml-2 text-sm text-gray-600">Richtig</span> <span class="ml-2 text-sm text-gray-600 dark:text-white">Richtig</span>
</label> </label>
<button type="button" onclick="removeOption(this)" <button type="button" onclick="removeOption(this)"
class="text-red-600 hover:text-red-800 transition-colors"> class="text-red-600 hover:text-red-800 transition-colors">
@@ -138,7 +138,7 @@
<div class="p-4"> <div class="p-4">
<label class="font-bold" for="time_per_question">Zeit pro Frage:</label> <label class="font-bold" for="time_per_question">Zeit pro Frage:</label>
<select name="time_per_question" id="time_per_question"> <select name="time_per_question" id="time_per_question" class="dark:text-white dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="10" {% if standard_time_per_question == "10" or time_per_question == "10" %}selected{% endif %}>10 Sekunden</option> <option value="10" {% if standard_time_per_question == "10" or time_per_question == "10" %}selected{% endif %}>10 Sekunden</option>
<option value="20" {% if standard_time_per_question == "20" or time_per_question == "20" %}selected{% endif %}>20 Sekunden</option> <option value="20" {% if standard_time_per_question == "20" or time_per_question == "20" %}selected{% endif %}>20 Sekunden</option>
<option value="30" {% if standard_time_per_question == "30" or time_per_question == "30" %}selected{% endif %}>30 Sekunden</option> <option value="30" {% if standard_time_per_question == "30" or time_per_question == "30" %}selected{% endif %}>30 Sekunden</option>
@@ -148,10 +148,10 @@
<option value="300" {% if standard_time_per_question == "300" or time_per_question == "300" %}selected{% endif %}>5 Minuten</option> <option value="300" {% if standard_time_per_question == "300" or time_per_question == "300" %}selected{% endif %}>5 Minuten</option>
</select> </select>
</div> </div>
<label for="credits" class="block text-sm font-medium text-gray-700">Credits</label> <label for="credits" class="block text-sm font-medium text-gray-700 dark:text-white">Credits</label>
<div class="mt-1"> <div class="mt-1">
<textarea name="credits" rows="4" <textarea name="credits" rows="4"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md "
>{{ credits }}</textarea> >{{ credits }}</textarea>
</div> </div>
@@ -183,12 +183,13 @@ function addOption() {
div.className = 'flex items-center space-x-3'; div.className = 'flex items-center space-x-3';
div.innerHTML = ` div.innerHTML = `
<textarea name="option_${optionCount}" rows="2" <textarea name="option_${optionCount}" rows="2"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md" class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></textarea> required>{{ option.value }}</textarea>
<label class="inline-flex items-center"> <label class="inline-flex items-center">
<input type="checkbox" name="correct_${optionCount}" value="1" <input type="checkbox" name="correct_${optionCount}" value="1"
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"> class=" focus:outline-none focus:ring-2 focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
<span class="ml-2 text-sm text-gray-600">Richtig</span> {% if option.is_correct %}checked{% endif %}>
<span class="ml-2 text-sm text-gray-600 dark:text-white">Richtig</span>
</label> </label>
<button type="button" onclick="removeOption(this)" <button type="button" onclick="removeOption(this)"
class="text-red-600 hover:text-red-800 transition-colors"> class="text-red-600 hover:text-red-800 transition-colors">

View File

@@ -0,0 +1,189 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mx-auto px-4">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold dark:text-white">{% if question %}Frage bearbeiten{% else %}Reihenfolge Frage{% endif %}</h1>
<a href="{% url 'library:detail_quiz' quiz.id %}"
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
Zurück zum Quiz
</a>
</div>
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent dark:text-white">
<form method="post" class="space-y-6" enctype="multipart/form-data">
{% csrf_token %}
<!-- Question Text -->
<div>
<label for="question" class="block text-sm font-medium text-gray-700 dark:text-white">Frage</label>
<div class="mt-1">
<textarea name="question" id="question" rows="4"
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
required>{{ question.data.question|default:'' }}</textarea>
</div>
</div>
{% if image %}
<img src="{{ image.image.url }}" style="max-width: 100px;" 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>
<input value="image.url" class="max-w-full bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400 dark:bg-transparent" type="file" name="question_image" id="question_image">
</div>
<!-- Options -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">richtige Reihenfolge</label>
<div id="options-container" class="space-y-3">
{% if question and question.data.options %}
{% for option in question.data.options|dictsort:"order" %}
<div class="flex items-center space-x-3">
<span class="number bg-blue-500 p-2 rounded-lg text-white">{{ forloop.counter }}.</span>
<textarea name="option_{{ forloop.counter }}" rows="2"
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required>{{ option.value }}</textarea>
<label class="inline-flex items-center">
</label>
<button type="button" onclick="removeOption(this)"
class="text-red-600 hover:text-red-800 transition-colors">
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
</div>
{% endfor %}
{% else %}
{% for i in "1234" %}
<div class="flex items-center space-x-3">
<span class="number bg-blue-500 p-2 rounded-lg text-white">{{ forloop.counter }}.</span>
<textarea name="option_{{ forloop.counter }}" rows="2"
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></textarea>
<label class="inline-flex items-center">
</label>
<button type="button" onclick="removeOption(this)"
class="text-red-600 hover:text-red-800 transition-colors">
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
</div>
{% endfor %}
{% endif %}
</div>
<button type="button" onclick="addOption()"
class="mt-3 inline-flex items-center px-3 py-1.5 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">
<svg class="h-4 w-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
Option hinzufügen
</button>
</div>
<div class="p-4">
<label class="font-bold" for="time_per_question">Zeit pro Frage:</label>
<select name="time_per_question" id="time_per_question" class="dark:text-white dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="10" {% if standard_time_per_question == "10" or time_per_question == "10" %}selected{% endif %}>10 Sekunden</option>
<option value="20" {% if standard_time_per_question == "20" or time_per_question == "20" %}selected{% endif %}>20 Sekunden</option>
<option value="30" {% if standard_time_per_question == "30" or time_per_question == "30" %}selected{% endif %}>30 Sekunden</option>
<option value="45" {% if standard_time_per_question == "45" or time_per_question == "45" %}selected{% endif %}>45 Sekunden</option>
<option value="60" {% if standard_time_per_question == "60" or time_per_question == "60" %}selected{% endif %}>1 Minute</option>
<option value="120" {% if standard_time_per_question == "120" or time_per_question == "120" %}selected{% endif %}>2 Minuten</option>
<option value="300" {% if standard_time_per_question == "300" or time_per_question == "300" %}selected{% endif %}>5 Minuten</option>
</select>
</div>
<label for="credits" class="block text-sm font-medium text-gray-700 dark:text-white">Credits</label>
<div class="mt-1">
<textarea name="credits" rows="4"
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md "
>{{ credits }}</textarea>
</div>
<div class="flex justify-end space-x-3">
<a href="{% url 'library:detail_quiz' quiz.id %}"
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">
Abbrechen
</a>
<button type="submit"
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
{% if question %}Speichern{% else %}Erstellen{% endif %}
</button>
</div>
</form>
</div>
</div>
<script>
function addOption() {
const container = document.getElementById('options-container');
const optionCount = container.children.length + 1;
if (optionCount > 6) {
alert('Maximal 6 Optionen sind erlaubt.');
return;
}
const div = document.createElement('div');
div.className = 'flex items-center space-x-3';
div.innerHTML = `
<span class="number bg-blue-500 p-2 rounded-lg text-white">${optionCount}.</span>
<textarea name="option_${optionCount}" rows="2"
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required></textarea>
<label class="inline-flex items-center">
</label>
<button type="button" onclick="removeOption(this)"
class="text-red-600 hover:text-red-800 transition-colors">
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
`;
container.appendChild(div);
}
function removeOption(button) {
const container = document.getElementById('options-container');
if (container.children.length > 2) {
button.parentElement.remove();
updateOptionNumbers();
} else {
alert('Mindestens zwei Optionen müssen vorhanden sein.');
}
}
function updateOptionNumbers() {
const container = document.getElementById('options-container');
const options = container.children;
Array.from(options).forEach((option, index) => {
const number = index + 1;
const textInput = option.querySelector('textarea');
textInput.name = `option_${number}`;
const numberSpan = option.querySelector('span.number');
if (numberSpan) {
numberSpan.textContent = `${number}.`;
}
});
}
</script>
{% endblock %}

View File

@@ -3,23 +3,23 @@
{% block content %} {% block content %}
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">{% if question %}Frage bearbeiten{% else %}Neue Wahr/Falsch Frage{% endif %}</h1> <h1 class="text-2xl font-bold dark:text-white">{% if question %}Frage bearbeiten{% else %}Neue Wahr/Falsch Frage{% endif %}</h1>
<a href="{% url 'library:detail_quiz' quiz.id %}" <a href="{% url 'library:detail_quiz' quiz.id %}"
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors"> class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
Zurück zum Quiz Zurück zum Quiz
</a> </a>
</div> </div>
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md"> <div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent dark:text-white">
<form method="post" class="space-y-6" enctype="multipart/form-data"> <form method="post" class="space-y-6" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<!-- Question Text --> <!-- Question Text -->
<div> <div>
<label for="question" class="block text-sm font-medium text-gray-700">Frage</label> <label for="question" class="block text-sm font-medium text-gray-700 dark:text-white">Frage</label>
<div class="mt-1"> <div class="mt-1">
<textarea name="question" id="question" rows="4" <textarea name="question" id="question" rows="4"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
required>{{ question.data.question }}</textarea> required>{{ question.data.question }}</textarea>
</div> </div>
</div> </div>
@@ -33,31 +33,31 @@
{% endif %} {% endif %}
<div> <div>
<label class="font-bold" for="question_image">Bild:</label> <label class="font-bold" for="question_image">Bild:</label>
<input class="bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400" type="file" name="question_image" id="question_image"> <input class="max-w-full bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400 dark:bg-transparent" type="file" name="question_image" id="question_image">
</div> </div>
<!-- True/False Selection --> <!-- True/False Selection -->
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-2">Richtige Antwort</label> <label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Richtige Antwort</label>
<div class="flex space-x-4"> <div class="flex space-x-4">
<label class="inline-flex items-center"> <label class="inline-flex items-center">
<input type="radio" name="correct_answer" value="true" <input type="radio" name="correct_answer" value="true"
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300" class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300"
{% if question.data.correct_answer %}checked{% endif %}> {% if question.data.correct_answer %}checked{% endif %}>
<span class="ml-2 text-sm text-gray-600">Wahr</span> <span class="ml-2 text-sm text-gray-600 dark:text-white">Wahr</span>
</label> </label>
<label class="inline-flex items-center"> <label class="inline-flex items-center">
<input type="radio" name="correct_answer" value="false" <input type="radio" name="correct_answer" value="false"
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300" class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300"
{% if not question.data.correct_answer %}checked{% endif %}> {% if not question.data.correct_answer %}checked{% endif %}>
<span class="ml-2 text-sm text-gray-600">Falsch</span> <span class="ml-2 text-sm text-gray-600 dark:text-white">Falsch</span>
</label> </label>
</div> </div>
</div> </div>
<div class="p-4"> <div class="p-4">
<label class="font-bold" for="time_per_question">Zeit pro Frage:</label> <label class="font-bold" for="time_per_question">Zeit pro Frage:</label>
<select name="time_per_question" id="time_per_question"> <select name="time_per_question" id="time_per_question" class="dark:text-white dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="10" {% if standard_time_per_question == "10" or time_per_question == "10" %}selected{% endif %}>10 Sekunden</option> <option value="10" {% if standard_time_per_question == "10" or time_per_question == "10" %}selected{% endif %}>10 Sekunden</option>
<option value="20" {% if standard_time_per_question == "20" or time_per_question == "20" %}selected{% endif %}>20 Sekunden</option> <option value="20" {% if standard_time_per_question == "20" or time_per_question == "20" %}selected{% endif %}>20 Sekunden</option>
<option value="30" {% if standard_time_per_question == "30" or time_per_question == "30" %}selected{% endif %}>30 Sekunden</option> <option value="30" {% if standard_time_per_question == "30" or time_per_question == "30" %}selected{% endif %}>30 Sekunden</option>
@@ -67,10 +67,10 @@
<option value="300" {% if standard_time_per_question == "300" or time_per_question == "300" %}selected{% endif %}>5 Minuten</option> <option value="300" {% if standard_time_per_question == "300" or time_per_question == "300" %}selected{% endif %}>5 Minuten</option>
</select> </select>
</div> </div>
<label for="credits" class="block text-sm font-medium text-gray-700">Credits</label> <label for="credits" class="block text-sm font-medium text-gray-700 dark:text-white">Credits</label>
<div class="mt-1"> <div class="mt-1">
<textarea name="credits" rows="4" <textarea name="credits" rows="4"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
>{{ credits }}</textarea> >{{ credits }}</textarea>
</div> </div>

View File

@@ -1,7 +1,48 @@
<div class="grid place-content-center gap-2 w-full mt-4"> <footer class="w-full bg-blue-600 text-white shadow-xl py-6 mt-24 break-words">
<div class="flex space-x-2 pb-4">
<p class="text-sm font-extralight"><a href="/impress">Impressum</a></p>
<p class="text-sm font-extralight"><a href="/privacy">Datenschutz</a></p>
<p class="text-sm font-extralight"><a href="https://edugit.org/enrichment-2024/qivip">Repository</a></p> <div class="flex items-center justify-center flex-col sm:flex-row sm:gap-32">
<div class="mb-2 sm:m-0">
<h1 class="flex justify-center text-md font-bold font-sans tracking-wider gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v17.25m0 0c-1.472 0-2.882.265-4.185.75M12 20.25c1.472 0 2.882.265 4.185.75M18.75 4.97A48.416 48.416 0 0 0 12 4.5c-2.291 0-4.545.16-6.75.47m13.5 0c1.01.143 2.01.317 3 .52m-3-.52 2.62 10.726c.122.499-.106 1.028-.589 1.202a5.988 5.988 0 0 1-2.031.352 5.988 5.988 0 0 1-2.031-.352c-.483-.174-.711-.703-.59-1.202L18.75 4.971Zm-16.5.52c.99-.203 1.99-.377 3-.52m0 0 2.62 10.726c.122.499-.106 1.028-.589 1.202a5.989 5.989 0 0 1-2.031.352 5.989 5.989 0 0 1-2.031-.352c-.483-.174-.711-.703-.59-1.202L5.25 4.971Z" />
</svg>
Rechtliches:</h1>
<div class="left-0 w-full p-2 flex justify-center space-x-2 text-white">
<p class="text-xs tracking-wide"><a class= "hover:underline" href="/impress">Impressum</a></p>
<p class="text-xs tracking-wide"><a class= "hover:underline" href="/privacy">Datenschutz</a></p>
<p class="text-xs tracking-wide"><a class= "hover:underline" href="https://edugit.org/enrichment-2024/qivip">Repository</a></p>
</div> </div>
</div> </div>
<div>
<h2 class="flex justify-center text-md font-bold font-sans tracking-widerd gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 7.5h1.5m-1.5 3h1.5m-7.5 3h7.5m-7.5 3h7.5m3-9h3.375c.621 0 1.125.504 1.125 1.125V18a2.25 2.25 0 0 1-2.25 2.25M16.5 7.5V18a2.25 2.25 0 0 0 2.25 2.25M16.5 7.5V4.875c0-.621-.504-1.125-1.125-1.125H4.125C3.504 3.75 3 4.254 3 4.875V18a2.25 2.25 0 0 0 2.25 2.25h13.5M6 7.5h3v3H6v-3Z" />
</svg>
Erfahren Sie mehr:</h2>
<div class="left-0 w-full p-2 flex justify-center space-x-2 text-white">
<p class="text-xs tracking-wide"><a class= "hover:underline" href="https://katharineum.de/qivip-unsere-neue-quiz-software/">
mehr über uns</a></p>
</div>
</div>
</div>
<div class="flex justify-center">
<div class="h-0.25 w-200 bg-white opacity-40 rounded-full mt-2">
</div></div>
<p class="text-xs text-center opacity-80 mt-2">
<a class="font-bold" href="/">qivip</a> - Ein Projekt entstanden während des Enrichment-Programms.
</p>
</footer>

View File

@@ -9,10 +9,10 @@
</div> </div>
<!-- Desktop Search (centered on screen) - Hidden on mobile --> <!-- 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"> <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 link|default:'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"> <form method="get" action="{% url link|default:'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 dark:bg-gray-900">
<input id="quiz-search-desktop" list="quiz-names-desktop" type="text" name="search" placeholder="Suche ..." value="{{ request.GET.search }}" <input id="quiz-search-desktop" list="quiz-names-desktop" 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"> class="text-sm w-full px-3 py-1 text-black bg-transparent focus:outline-none lg:text-base dark:text-white">
<button class="py-1 text-blue-600"> <button class="py-1 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 md:size-6"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5 md:size-6">
<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" /> <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" />
@@ -48,13 +48,13 @@
</div> </div>
<!-- Mobile Navigation Menu - Hidden by default - FULLWIDTH DROPDOWN --> <!-- 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 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"> <div class="bg-blue-500 pt-3 pb-4 rounded-b-lg shadow-inner">
<!-- Search in mobile menu --> <!-- Search in mobile menu -->
<form method="get" action="{% url link|default:'library:overview_quiz' %}" class="px-4 mb-4"> <form method="get" action="{% url link|default:'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"> <div class="flex items-center justify-center bg-white rounded-full px-4 py-2 shadow-md dark:bg-gray-900">
<input id="quiz-search-mobile" list="quiz-names-mobile" type="text" name="search" placeholder="Suche ..." value="{{ request.GET.search }}" <input id="quiz-search-mobile" list="quiz-names-mobile" type="text" name="search" placeholder="Suche ..." value="{{ request.GET.search }}"
class="w-full text-sm px-2 text-black bg-transparent focus:outline-none"> class="w-full text-sm px-2 text-black bg-transparent focus:outline-none dark:text-white">
<button class="text-blue-600"> <button class="text-blue-600">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-5"> stroke="currentColor" class="size-5">

View File

@@ -4,16 +4,16 @@
{% block content %} {% block content %}
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<div class="bg-white rounded-lg shadow-md p-8 mb-6"> <div class="bg-white rounded-lg shadow-md p-8 mb-6 dark:bg-transparent dark:text-white">
<div class="text-center mb-8"> <div class="text-center mb-8">
<h1 class="text-4xl font-bold text-blue-600 mb-4">Quiz beendet!</h1> <h1 class="text-4xl font-bold text-blue-600 mb-4">Quiz beendet!</h1>
<p class="text-xl text-gray-600">Vielen Dank fürs Spielen!</p> <p class="text-xl text-gray-600 dark:text-white">Vielen Dank fürs Spielen!</p>
</div> </div>
{% if not is_host %} {% if not is_host %}
{% if rating_enabled %} {% if rating_enabled %}
<div class="bg-blue-50 rounded-lg p-6 mb-8"> <div class="bg-blue-50 rounded-lg p-6 mb-8 dark:bg-transparent ">
<h2 class="text-2xl font-bold text-blue-800 mb-4 text-center">Wie hat dir das Quiz gefallen?</h2> <h2 class="text-2xl font-bold text-blue-800 mb-4 text-center dark:text-white">Wie hat dir das Quiz gefallen?</h2>
<div class="stars flex justify-center space-x-4 mb-4"> <div class="stars flex justify-center space-x-4 mb-4">
<i class="fas fa-star text-3xl transition-all duration-200 hover:scale-110" data-rating="1"></i> <i class="fas fa-star text-3xl transition-all duration-200 hover:scale-110" data-rating="1"></i>
<i class="fas fa-star text-3xl transition-all duration-200 hover:scale-110" data-rating="2"></i> <i class="fas fa-star text-3xl transition-all duration-200 hover:scale-110" data-rating="2"></i>

View File

@@ -1,6 +1,8 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
{% load static %}
<script src="{% static 'js/sortablejs/Sortable.min.js' %}"></script>
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
@@ -11,17 +13,17 @@
</button> </button>
{% endif %} {% endif %}
<button onclick="this.onclick=null; advanceToScores();" class="mx-2 bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded"> <button onclick="this.onclick=null; advanceToScores();" class="mx-2 bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded">
weiter überspringen
</button> </button>
</div> </div>
<div id="form_host" class="bg-white rounded-lg shadow-md p-4 mb-6"> <div id="form_host" class="bg-white rounded-lg shadow-md p-4 mb-6 dark:bg-transparent dark:text-white">
<div class="flex justify-between mb-2"> <div class="flex justify-between mb-2">
<div class="flex gap-1 text-sm"><p class="text-sm">Frage</p> <div class="flex gap-1 text-sm"><p class="text-sm">Frage</p>
<p class="text-gray-700 font-bold text-sm">{{ question_index|add:1 }}</p> <p class="text-gray-700 font-bold text-sm dark:text-white">{{ question_index|add:1 }}</p>
<p class="text-sm">von</p> <p class="text-sm">von</p>
<p class="text-gray-700 font-bold text-sm">{{ total_questions }}</p> <p class="text-gray-700 font-bold text-sm dark:text-white">{{ total_questions }}</p>
</div> </div>
<div class="flex gap-1 mb-2"><p class="text-sm">Antworten:</p> <p id="answer-count" class="text-gray-700 font-bold text-sm"></p></div> <div class="flex gap-1 mb-2"><p class="text-sm">Antworten:</p> <p id="answer-count" class="text-gray-700 font-bold text-sm dark:text-white"></p></div>
</div> </div>
@@ -48,7 +50,7 @@
</div> </div>
{% if question_data.type != "input" %} {% if question_data.type == "multiple_choice" or question_data.type == "true_false" %}
<div class="grid grid-cols-2 gap-4" id="options-container"> <div class="grid grid-cols-2 gap-4" id="options-container">
{% for option in question_data.options %} {% for option in question_data.options %}
<button <button
@@ -81,25 +83,201 @@
onclick="submitAnswer({{ forloop.counter0 }});" {% endif %}> onclick="submitAnswer({{ forloop.counter0 }});" {% endif %}>
<p class="text-lg">{{ option.value }}</p> <p class="text-lg">{{ option.value }}</p>
</button> </button>
{% endfor %} {% endfor %}
</div> </div>
{% elif question_data.type == "order" %}
<div id="sortable-options" class=" mb-4 flex flex-col items-center">
{% for option in question_data.options %}
<button data-index="{{ forloop.counter0 }}"
data-value="{{ option.value }}"
class=" my-2 w-3/4 max-w-xl
{% if forloop.counter0 == 0 %}
bg-green-500
hover:bg-green-600
{% elif forloop.counter0 == 1 %}
bg-red-500
hover:bg-red-600
{% elif forloop.counter0 == 2 %}
bg-blue-500
hover:bg-blue-600
{% elif forloop.counter0 == 3 %}
bg-purple-500
hover:bg-purple-600
{% elif forloop.counter0 == 4 %}
bg-orange-500
hover:bg-orange-600
{% elif forloop.counter0 == 5 %}
bg-yellow-500
hover:bg-yellow-600
{% else %} {% else %}
bg-gray-500
hover:bg-gray-600
{% endif %}
p-4 rounded-lg text-white transition-colors duration-200 answer-option overflow-auto break-words hyphens-auto "
data-index="{{ forloop.counter0 }}"
>
<p class="text-lg flex items-center">
<span class="order-number inline-block w-8 h-8 rounded-full bg-white text-gray-800 font-bold text-center leading-8 mr-3">1</span>
{{ option.value }}
</p>
</button>
{% endfor %}
</div>
<div class="flex gap-2">
{% if request.session.mode == "learn" %}
<button
onclick="submitOrderAnswer()"
id="send_order" class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-lg transition-colors duration-200 answer-option "
>
Abschicken
</button>
{% endif %}
{% elif question_data.type == "input" %}
{% if request.session.mode == "learn" %} {% if request.session.mode == "learn" %}
<form action=""> <form action="">
<input id="textanswer" type="text" placeholder="DEINE ANTWORT" <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 "> 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 {% if request.session.mode == "learn" %} <button
onclick="submitAnswer( -1 );" {% endif %} onclick="submitAnswer( -1 );"
class="mt-2 text-center p-2 bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 answer-option"> class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-lg transition-colors duration-200 answer-option "
abschicken >
Abschicken
</button> </button>
</form> </form>
{% endif %} {% endif %}
{% elif question_data.type == "map" and request.session.mode == "learn" %}
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<div class="text-sm text-gray-600 dark:text-gray-400 mb-2">Ziel auswählen</div>
<div id="map" class="h-96"></div>
<input type="hidden" value="" id="singleanswer" name="singleanswer">
<div class="mt-4">
<button onclick="submitAnswerMap()" class="bg-blue-400 text-white px-4 py-2 rounded-md hover:bg-blue-500 transition-colors">
Abgeben
</button>
</div>
<input type="hidden" id="location" name="location" value="">
<script>
let marker = null;
const locationInput = document.getElementById('location');
const map = L.map('map').setView([51.72, 10.45], 3);
// Prüfe, ob Dark Mode aktiv ist
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
// Wähle das passende Karten-Design
const tileUrl = isDarkMode
? 'https://{s}.basemaps.cartocdn.com/rastertiles/dark_nolabels/{z}/{x}/{y}.png'
: 'https://{s}.basemaps.cartocdn.com/rastertiles/light_nolabels/{z}/{x}/{y}.png';
L.tileLayer(tileUrl, {
maxZoom: 19,
attribution: '© OpenStreetMap © CARTO'
}).addTo(map);
function updateInput(latlng) {
locationInput.value = `${latlng.lat.toFixed(6)},${latlng.lng.toFixed(6)}`;
}
function placeOrMoveMarker(latlng) {
if (marker) {
marker.setLatLng(latlng);
} else {
marker = L.marker(latlng, { draggable: true }).addTo(map);
marker.on('dragend', () => {
updateInput(marker.getLatLng());
});
}
updateInput(latlng);
}
map.on('click', e => placeOrMoveMarker(e.latlng));
function submitAnswerMap() {
const answer = document.getElementById("location").value;
if (answer === "") {
alert("Bitte einen Ort auf der Karte auswählen!");
return;
}
// Add grey overlay
const overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
overlay.style.zIndex = '1000';
overlay.style.display = 'flex';
overlay.style.justifyContent = 'center';
overlay.style.alignItems = 'center';
overlay.style.color = 'white';
overlay.style.fontSize = '24px';
overlay.style.fontWeight = 'bold';
overlay.textContent = 'Antwort übermittelt';
document.body.appendChild(overlay);
const now = Date.now();
const timeRemaining = Math.max(0, questionDuration - (now - questionStartTime));
// Submit the answer
submitAnswer(answer);
}
</script>
{% elif question_data.type == "map" and request.session.mode != "learn" %}
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2 text-center">Bitte den gesuchten Ort auf der Karte auswählen</p>
{% elif question_data.type == "guess" %}
{% if request.session.mode == "learn" %}
<form class="flex flex-col items-center gap-4 mt-4">
<!-- Slider mit Beschriftung -->
<div class="w-full max-w-xl text-center">
<input
id="textanswer"
type="range"
min="{{ question_data.min }}"
max="{{ question_data.max }}"
value="{{ question_data.min }}"
step="1"
oninput="document.getElementById('range-value').textContent = 'Wert: ' + this.value;"
class="w-full answer-option bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-[#2a2f3a]"
>
<p id="range-value" class="mt-2 font-bold text-gray-800 dark:text-white">Wert: {{ question_data.min }}</p>
</div>
<!-- Abschicken-Button -->
<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 "
>
Abschicken
</button>
</form>
{% endif %}
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
@@ -131,8 +309,12 @@
} else if (data.type === 'game_state_update' && } else if (data.type === 'game_state_update' &&
data.action === 'show_scores' && data.action === 'show_scores' &&
data.redirect_url) { data.redirect_url) {
setTimeout(() => {
submitAnswer(-1);
window.location.href = data.redirect_url; window.location.href = data.redirect_url;
submitAnswer( -1 ); }, 1000);
} }
}; };
@@ -180,7 +362,7 @@
'type': 'advance_to_scores', 'type': 'advance_to_scores',
'host_id': hostId 'host_id': hostId
})); }));
}, 2000); }, 0);
} }
// Timer // Timer
@@ -199,6 +381,7 @@
if (remaining === 0) { if (remaining === 0) {
advanceToScores(); advanceToScores();
} else { } else {
requestAnimationFrame(updateTimer); requestAnimationFrame(updateTimer);
} }
@@ -281,5 +464,74 @@
})); }));
} }
} }
let initialOrder = [];
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('sortable-options');
if (container) {
initialOrder = Array.from(container.children).map(div => div.dataset.index);
Sortable.create(container, {
animation: 150,
onUpdate: updateOrderNumbers // <--- Nummern aktualisieren nach Sortieren
});
updateOrderNumbers(); // initial setzen
}
});
function updateOrderNumbers() {
const container = document.getElementById('sortable-options');
const children = container.children;
for (let i = 0; i < children.length; i++) {
const label = children[i].querySelector('.order-number');
if (label) {
label.textContent = `${i + 1}.`;
}
}
}
function submitOrderAnswer() {
const order_button=document.getElementById("send_order")
order_button.disabled=true;
order_button.classList.add('opacity-50');
const orderedDivs = document.querySelectorAll('#sortable-options button');
const selectedOrder = Array.from(orderedDivs).map(div => div.dataset.value);
const now = Date.now();
let timeRemaining = Math.max(0, questionDuration - (now - questionStartTime));
{% if request.session.mode == "learn" %}
timeRemaining = questionDuration;
{% endif %}
gameSocket.send(JSON.stringify({
type: 'submit_answer',
participant_id: '{{ host_id }}',
answer: selectedOrder,
time_remaining: timeRemaining
}));
hasAnswered = true;
}
function resetOrderSelection() {
const container = document.getElementById('sortable-options');
const divs = Array.from(container.children);
const indexMap = {};
divs.forEach(div => {
indexMap[div.dataset.index] = div;
});
// Sort children back to initial order
container.innerHTML = '';
initialOrder.forEach(i => {
container.appendChild(indexMap[i]);
});
}
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -1,19 +1,23 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
{% load static %}
<script src="{% static 'js/sortablejs/Sortable.min.js' %}"></script>
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-end mb-4"> <div class="flex justify-end mb-4">
<button onclick="leaveGame()" class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded"> <button onclick="leaveGame()" class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">
Spiel verlassen Spiel verlassen
</button> </button>
</div> </div>
<div class="bg-white rounded-lg shadow-md p-6 mb-6"> <div class="bg-white rounded-lg shadow-md p-6 mb-6 dark:bg-transparent dark:text-white">
<div class="flex justify-between mb-2"> <div class="flex justify-between mb-2">
<div class="flex gap-1 text-sm"><p class="text-sm">Frage</p> <div class="flex gap-1 text-sm"><p class="text-sm">Frage</p>
<p class="text-gray-700 font-bold text-sm">{{ question_index|add:1 }}</p> <p class="text-gray-700 font-bold text-sm dark:text-white">{{ question_index|add:1 }}</p>
<p class="text-sm">von</p> <p class="text-sm">von</p>
<p class="text-gray-700 font-bold text-sm">{{ total_questions }}</p> <p class="text-gray-700 font-bold text-sm dark:text-white">{{ total_questions }}</p>
</div> </div>
</div> </div>
<h1 class="text-xl font-black mb-2 border-blue-500 border-4 p-4 text-center rounded-lg md:text-2xl lg:text-4xl">{{ question_data.question}}</h1> <h1 class="text-xl font-black mb-2 border-blue-500 border-4 p-4 text-center rounded-lg md:text-2xl lg:text-4xl">{{ question_data.question}}</h1>
@@ -21,11 +25,11 @@
<div class="w-full bg-gray-300 rounded-full h-4 my-4"> <div class="w-full bg-gray-300 rounded-full h-4 my-4">
<div id="time-bar" class="bg-blue-600 h-4 rounded-full transition-all duration-100 linear" style="width: 100%;"></div> <div id="time-bar" class="bg-blue-600 h-4 rounded-full transition-all duration-100 linear" style="width: 100%;"></div>
</div> </div>
{% if question_data.type == "multiple_choice" or question_data.type == "true_false" %}
{% if question_data.type != "input" %}
<div class="grid grid-cols-2 gap-4" id="options-container"> <div class="grid grid-cols-2 gap-4" id="options-container">
{% for option in question_data.options %} {% for option in question_data.options %}
<button <button data-index="{{ forloop.counter0 }}"
data-value="{{ option.value }}"
class=" class="
{% if forloop.counter0 == 0 %} {% if forloop.counter0 == 0 %}
bg-green-500 bg-green-500
@@ -56,16 +60,165 @@
</button> </button>
{% endfor %} {% endfor %}
</div> </div>
{% elif question_data.type == "order" %}
<div id="sortable-options" class=" mb-4 flex flex-col items-center">
{% for option in question_data.options %}
<button data-index="{{ forloop.counter0 }}"
data-value="{{ option.value }}"
class=" my-2 w-3/4 max-w-xl
{% if forloop.counter0 == 0 %}
bg-green-500
hover:bg-green-600
{% elif forloop.counter0 == 1 %}
bg-red-500
hover:bg-red-600
{% elif forloop.counter0 == 2 %}
bg-blue-500
hover:bg-blue-600
{% elif forloop.counter0 == 3 %}
bg-purple-500
hover:bg-purple-600
{% elif forloop.counter0 == 4 %}
bg-orange-500
hover:bg-orange-600
{% elif forloop.counter0 == 5 %}
bg-yellow-500
hover:bg-yellow-600
{% else %} {% else %}
bg-gray-500
hover:bg-gray-600
{% endif %}
p-4 rounded-lg text-white transition-colors duration-200 answer-option overflow-auto break-words hyphens-auto "
data-index="{{ forloop.counter0 }}"
>
<p class="text-lg flex items-center">
<span class="order-number inline-block w-8 h-8 rounded-full bg-white text-gray-800 font-bold text-center leading-8 mr-3">1</span>
{{ option.value }}
</p>
</button>
{% endfor %}
</div>
<div class="flex gap-2">
<button id="send_order"
onclick="submitOrderAnswer()"
class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-lg transition-colors duration-200 answer-option "
>
Abschicken
</button>
{% elif question_data.type == "guess" %}
<form class="flex flex-col items-center gap-4 mt-4">
<!-- Slider mit Beschriftung -->
<div class="w-full max-w-xl text-center">
<input
id="textanswer"
type="range"
min="{{ question_data.min }}"
max="{{ question_data.max }}"
value="{{ question_data.min }}"
step="1"
oninput="document.getElementById('range-value').textContent = 'Wert: ' + this.value;"
class="w-full answer-option bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-[#2a2f3a]"
>
<p id="range-value" class="mt-2 font-bold text-gray-800 dark:text-white">Wert: {{ question_data.min }}</p>
</div>
<!-- Abschicken-Button -->
<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 "
>
Abschicken
</button>
</form>
{% elif question_data.type == "input" %}
<form action=""> <form action="">
<input id="textanswer" type="text" placeholder="DEINE ANTWORT" <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"> 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 <button
onclick="submitAnswer( -1 );" onclick="submitAnswer( -1 );"
class="mt-2 text-center p-2 bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 answer-option"> class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-lg transition-colors duration-200 answer-option "
abschicken >
Abschicken
</button> </button>
</form> </form>
{% elif question_data.type == "map" %}
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<div class="text-sm text-gray-600 dark:text-gray-400 mb-2">Ziel auswählen</div>
<div id="map" class="h-96"></div>
<input type="hidden" value="" id="location" name="location">
<div class="mt-4">
<button onclick="submitAnswerMap()" class="bg-blue-400 text-white px-4 py-2 rounded-md hover:bg-blue-500 transition-colors">
Abgeben
</button>
</div>
<script>
let marker = null;
const locationInput = document.getElementById('location');
const map = L.map('map').setView([51.72, 10.45], 3);
// Prüfe, ob Dark Mode aktiv ist
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
// Wähle das passende Karten-Design
const tileUrl = isDarkMode
? 'https://{s}.basemaps.cartocdn.com/rastertiles/dark_nolabels/{z}/{x}/{y}.png'
: 'https://{s}.basemaps.cartocdn.com/rastertiles/light_nolabels/{z}/{x}/{y}.png';
L.tileLayer(tileUrl, {
maxZoom: 19,
attribution: '© OpenStreetMap © CARTO'
}).addTo(map);
function updateInput(latlng) {
locationInput.value = `${latlng.lat.toFixed(6)},${latlng.lng.toFixed(6)}`;
}
function placeOrMoveMarker(latlng) {
if (marker) {
marker.setLatLng(latlng);
} else {
marker = L.marker(latlng, { draggable: true }).addTo(map);
marker.on('dragend', () => {
updateInput(marker.getLatLng());
});
}
updateInput(latlng);
}
map.on('click', e => placeOrMoveMarker(e.latlng));
</script>
{% else %}
<p>aaaaah gibts nicht</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -89,7 +242,11 @@
data.action === 'show_scores' && data.action === 'show_scores' &&
data.redirect_url) { data.redirect_url) {
window.location.href = data.redirect_url; window.location.href = data.redirect_url;
submitAnswer( -1 ); {% if question_data.type == "order" %}
submitOrderAnswer();
{% elif question_data.type != "map" %}
submitAnswer(-1);
{% endif %}
} }
}; };
@@ -97,7 +254,22 @@
wsUtil.handleClose(e); wsUtil.handleClose(e);
}; };
function submitAnswer(optionIndex) { function submitAnswer(optionIndex=0, has_options=true) {
if (!has_options){
answer = document.getElementById("singleanswer").value;
if (answer == ""){
alert("Bitte eine Antwort eingeben!");
return;
}
gameSocket.send(JSON.stringify({
type: 'submit_answer',
participant_id: participantId,
answer:answer,
time_remaining: questionDuration
}));
}
if (hasAnswered) return; if (hasAnswered) return;
let input = ""; // Declare input here let input = ""; // Declare input here
@@ -143,7 +315,42 @@
} }
} }
function submitAnswerMap() {
answer = document.getElementById("location").value;
if (answer == ""){
alert("Bitte eine Antwort eingeben!");
return;
}
// Add grey overlay
const overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
overlay.style.zIndex = '1000';
overlay.style.display = 'flex';
overlay.style.justifyContent = 'center';
overlay.style.alignItems = 'center';
overlay.style.color = 'white';
overlay.style.fontSize = '24px';
overlay.style.fontWeight = 'bold';
overlay.textContent = 'Antwort übermittelt';
document.body.appendChild(overlay);
const now = Date.now();
const timeRemaining = Math.max(0, questionDuration - (now - questionStartTime));
gameSocket.send(JSON.stringify({
type: 'submit_answer',
participant_id: participantId,
answer: answer,
time_remaining: timeRemaining
}));
}
// Timer // Timer
function updateTimer() { function updateTimer() {
@@ -159,7 +366,13 @@
if (remaining === 0 && !hasAnswered) { if (remaining === 0 && !hasAnswered) {
// Auto-submit timeout answer // Auto-submit timeout answer
{% if question_data.type == "order" %}
submitOrderAnswer();
{% else %}
submitAnswer(-1); submitAnswer(-1);
{% endif %}
} else if (remaining > 0) { } else if (remaining > 0) {
requestAnimationFrame(updateTimer); requestAnimationFrame(updateTimer);
} }
@@ -176,5 +389,75 @@
// Start timer when page loads // Start timer when page loads
updateTimer(); updateTimer();
let initialOrder = [];
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('sortable-options');
if (container) {
initialOrder = Array.from(container.children).map(div => div.dataset.index);
Sortable.create(container, {
animation: 150,
onUpdate: updateOrderNumbers // <--- Nummern aktualisieren nach Sortieren
});
updateOrderNumbers(); // initial setzen
}
});
function updateOrderNumbers() {
const container = document.getElementById('sortable-options');
const children = container.children;
for (let i = 0; i < children.length; i++) {
const label = children[i].querySelector('.order-number');
if (label) {
label.textContent = `${i + 1}.`;
}
}
}
function submitOrderAnswer() {
if (hasAnswered) return;
const order_button=document.getElementById("send_order")
order_button.disabled=true;
order_button.classList.add('opacity-50');
const orderedDivs = document.querySelectorAll('#sortable-options button');
const selectedOrder = Array.from(orderedDivs).map(div => div.dataset.value);
const now = Date.now();
let timeRemaining = Math.max(0, questionDuration - (now - questionStartTime));
{% if request.session.mode == "learn" %}
timeRemaining = questionDuration;
{% endif %}
gameSocket.send(JSON.stringify({
type: 'submit_answer',
participant_id: participantId,
answer: selectedOrder,
time_remaining: timeRemaining
}));
hasAnswered = true;
}
function resetOrderSelection() {
const container = document.getElementById('sortable-options');
const divs = Array.from(container.children);
const indexMap = {};
divs.forEach(div => {
indexMap[div.dataset.index] = div;
});
// Sort children back to initial order
container.innerHTML = '';
initialOrder.forEach(i => {
container.appendChild(indexMap[i]);
});
}
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -2,35 +2,242 @@
{% block content %} {% block content %}
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="bg-white rounded-lg shadow-md p-6 mb-6"> <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> <h1 class="text-3xl font-bold mb-6">Ergebnisse</h1>
{% if request.session.mode == "learn" or is_host %}
<div class="mb-6"> <div class="mb-6">
<h2 class="text-xl font-bold mb-4">Aktuelle Frage</h2> <h2 class="text-xl font-bold mb-4">Aktuelle Frage</h2>
<p class="text-lg mb-2">{{ question_data.question }}</p> <p class="text-lg mb-2">{{ question_data.question }}</p>
{% if question_data.type == "map" %}
<!-- Map Question Display -->
<div class="mb-4">
<div id="map" class="h-96 w-full rounded-lg border border-gray-300 dark:border-gray-600"></div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<script>
const participantAnswers = [
{% for participant in participants %}
{% if participant.coordinate_answer %}
"{{ participant.coordinate_answer }}",
{% endif %}
{% endfor %}];
document.addEventListener('DOMContentLoaded', function() {
// Parse the target location from the string format "lat,lng"
const targetCoords = '{{ question_data.target_location|escapejs }}'.split(',');
let targetLat, targetLng;
if (targetCoords.length === 2) {
targetLat = parseFloat(targetCoords[0].trim());
targetLng = parseFloat(targetCoords[1].trim());
// Normalize longitude to be within -180 to 180
targetLng = ((targetLng % 360) + 540) % 360 - 180;
// Clamp latitude to valid range (-90 to 90)
targetLat = Math.max(-90, Math.min(90, targetLat));
console.log('Normalized coordinates:', { lat: targetLat, lng: targetLng });
}
// Validate coordinates
if (isNaN(targetLat) || isNaN(targetLng)) {
console.warn('Invalid coordinates, hiding map');
document.getElementById('map-container').style.display = 'none';
return; // Exit the function early
}
const targetLocation = [targetLat, targetLng];
console.log('Using coordinates:', targetLocation);
// Initialize map centered on target location
const map = L.map('map').setView(targetLocation, 7);
// Check for dark mode
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
// Choose appropriate map style
const tileUrl = isDarkMode
? 'https://{s}.basemaps.cartocdn.com/rastertiles/dark_all/{z}/{x}/{y}.png'
: 'https://{s}.basemaps.cartocdn.com/rastertiles/light_all/{z}/{x}/{y}.png';
L.tileLayer(tileUrl, {
maxZoom: 19,
attribution: '© OpenStreetMap © CARTO',
noWrap: true
}).addTo(map);
participantAnswers.forEach(coordStr => {
const coords = coordStr.split(',').map(Number);
if (coords.length === 2 && !coords.some(isNaN)) {
const marker = L.marker(coords).addTo(map).bindPopup('Antwort eines Teilnehmers');
// Linie vom Ziel zum Teilnehmermarker
L.polyline([targetLocation, coords], {
color: '#f59e0b',
weight: 2, // Linienstärke
dashArray: '5,5', // gestrichelte Linie
opacity: 0.6
}).addTo(map);
}
});
// Add target marker
L.marker(targetLocation, {
icon: L.divIcon({
html: '🎯',
className: 'text-3xl',
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -32]
})
}).addTo(map)
.bindPopup('Zielort')
.openPopup();
// Add tolerance radius circle if available
{% if question_data.tolerance_radius %}
(function() {
try {
const toleranceRadius = parseFloat('{{ question_data.tolerance_radius|escapejs }}'.replace(',', '.'));
if (!isNaN(toleranceRadius) && toleranceRadius > 0) {
L.circle(targetLocation, {
color: isDarkMode ? '#3b82f6' : '#2563eb',
fillColor: isDarkMode ? '#3b82f6' : '#2563eb',
fillOpacity: 0.2,
radius: toleranceRadius
}).addTo(map);
console.log('Added tolerance radius:', toleranceRadius);
}
} catch (e) {
console.error('Error adding tolerance radius:', e);
}
})();
{% endif %}
});
</script>
{% elif question_data.type != "order" and question_data.type != "guess" %}
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
{% for option in question_data.options %} {% for option in question_data.options %}
<div id="option-box-{{ forloop.counter0 }}" class="bg-gray-100 relative p-4 rounded-lg overflow-auto break-words hyphens-auto {% if option.is_correct %} border-2 border-green-500{% else %}border-2 border-red-500{% endif %}"> <div id="option-box-{{ forloop.counter0 }}" class=" dark:bg-transparent relative p-4 rounded-lg overflow-auto break-words hyphens-auto {% if option.is_correct %} border-2 border-green-500{% else %}border-2 border-red-500{% endif %}">
<div id="option-fill-{{ forloop.counter0 }}" class="absolute left-0 top-0 h-full {% if option.is_correct %}bg-green-300 {% else %}bg-red-300 {% endif %} opacity-50 z-0 transition-all duration-1500 " style="width: 0%;"></div> <div id="option-fill-{{ forloop.counter0 }}" class="absolute left-0 top-0 h-full {% if option.is_correct %}bg-green-300 {% else %}bg-red-300 {% endif %} opacity-50 z-0 transition-all duration-1500 " style="width: 0%;"></div>
<div class="relative z-10"> <div class="relative z-10">
<p class="text-lg">{{ option.value }}</p> <p class="text-lg ">{{ option.value }}</p>
<p class="text-gray-600" id="option-count-{{ forloop.counter0 }}">0 Antworten</p> <p class="text-gray-600 dark:text-gray-400" id="option-count-{{ forloop.counter0 }}">0 Antworten</p>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% elif question_data.type == "guess" %}
<div id="option-box-0" class="relative p-4 rounded-lg border-2 border-green-500 overflow-auto">
<div id="option-fill-0" class="absolute left-0 top-0 h-full bg-green-300 opacity-50 z-0 transition-all duration-1500" style="width: 0%;"></div>
<div class="relative z-10">
<p class="text-lg">Richtige Anwort: {% for option in question_data.options%} {{ option.value }}{% endfor %}</p>
<p class="text-sm">(prozentuale Punktevergabe ab Genauigkeit von {{ question_data.tolerance }}%)</p>
<p class="text-gray-600" id="option-count-0">0 Antworten</p>
</div>
</div>
<div id="option-box-1" class="relative p-4 rounded-lg border-2 border-red-500 overflow-auto mt-2">
<div id="option-fill-1" class="absolute left-0 top-0 h-full bg-red-300 opacity-50 z-0 transition-all duration-1500" style="width: 0%;"></div>
<div class="relative z-10">
<p class="text-lg">Schätzung zu ungenau</p>
<p class="text-gray-600" id="option-count-1">0 Antworten</p>
</div>
</div>
{% else %}
<div id="option-box-0" class="relative p-4 rounded-lg border-2 border-green-500 overflow-auto">
<div id="option-fill-0" class="absolute left-0 top-0 h-full bg-green-300 opacity-50 z-0 transition-all duration-1500" style="width: 0%;"></div>
<div class="relative z-10">
<p class="text-lg">Richtige Reihenfolge</p>
<ul class="">
{% for option in question_data.options|dictsort:"order" %}
<li class="text-lg"> {{ forloop.counter }}. <span class="">{{ option.value }}</span> </li>
{% endfor %}
</ul>
<p class="text-gray-600" id="option-count-0">0 Antworten</p>
</div>
</div>
<div id="option-box-1" class="relative p-4 rounded-lg border-2 border-red-500 overflow-auto mt-2">
<div id="option-fill-1" class="absolute left-0 top-0 h-full bg-red-300 opacity-50 z-0 transition-all duration-1500" style="width: 0%;"></div>
<div class="relative z-10">
<p class="text-lg">Falsche Reihenfolge</p>
<p class="text-gray-600" id="option-count-1">0 Antworten</p>
</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>
<div class="mb-6"> <div class="mb-6">
{% if request.session.mode == "learn" %}
<h2 class="text-xl font-bold mb-4">Punktestand</h2> <h2 class="text-xl font-bold mb-4">Punktestand</h2>
{% endif %}
<div id="scoreboard" class="leading-relaxed"> <div id="scoreboard" class="leading-relaxed">
{% for participant in participants %} {% for participant in participants %}
{% if request.session.mode == "learn" %}
{% if participant == my_participant or is_host %} {% if participant == my_participant or is_host %}
<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 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"> <div class="flex flex-col items-start gap-1">
<div class="text-lg font-bold flex items-center gap-2"> <div class="text-lg font-bold flex items-center gap-2">
{% if forloop.counter == 1 %} {% if forloop.counter == 1 %}
@@ -42,16 +249,115 @@
{% else %} {% else %}
{{ forloop.counter }}. {{ forloop.counter }}.
{% endif %} {% endif %}
<span class="display_name text-gray-600 font-black text-2xl " id="display_name-{{ forloop.counter0 }}">{{ participant.display_name }}</span> <span class="display_name text-gray-600 font-black text-2xl dark:text-white " id="display_name-{{ forloop.counter0 }}">{{ participant.display_name }}</span>
</div class="flex flex-col items-start"> </div class="flex flex-col items-start">
<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> <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"%} {% if participant == my_participant or request.session.mode == "learn"%}
<p class="text-blue-600 font-bold text-sm">+{{ participant.last_score }} Punkte</p> <p class="text-blue-600 font-bold text-sm">+{{ participant.last_score }} Punkte</p>
{% endif %}
{% endif %}
</div>
{% 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="dark:text-white pt-3 text-center text-gray-600 font-bold" id="antwort-richtig">
Hier erscheint gleich dein Feedback...
</div>
</div>
{% elif participant.last_answer_correct == False %}
<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="dark:text-white 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>
<script>
{% 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 %}
{% 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>
</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>
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() { async function start() {
elements=document.getElementsByClassName("show-score"); elements=document.getElementsByClassName("show-score");
@@ -87,153 +393,17 @@
{% if is_last_question %} {% if is_last_question %}
start();
bounce(); bounce();
{% if request.session.mode == "learn" %}
triggerConfetti();
{% endif %}
{% else %} {% else %}
start(); start();
{% endif %} {% 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>
{% elif participant.last_answer_correct == False %}
<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 %}
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% if is_host %}
{% if is_last_question %}
<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>
{% 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 %}
<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 %}
{% include 'play/game/sound.html' %}
{% endif %}
<script>
const richtigeAntworten = [ const richtigeAntworten = [
"Deine Antwort ist nicht falsch. Sie ist richtig!", "Deine Antwort ist nicht falsch. Sie ist richtig!",
"Ja, du kannst es!", "Ja, du kannst es!",
@@ -308,9 +478,10 @@ if (antwortRichtig) {
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
if (data.type === 'game_state_update' && if (data.type === 'game_state_update' &&
data.redirect_url && data.redirect_url &&
(data.action === 'next_question' || data.action === 'finish_game')) { (data.action === 'next_question' || data.action === 'finish_game' || data.action === 'show_winner_podest'|| data.action === 'show_scoreboard')) {
window.location.href = data.redirect_url; window.location.href = data.redirect_url;
} else if (data.type === 'answer_stats') { }
else if (data.type === 'answer_stats') {
updateAnswerStats(data.stats); updateAnswerStats(data.stats);
} }
}; };
@@ -336,7 +507,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 %} {% 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'); var nextButton = document.getElementById('next-button');
if (nextButton) { if (nextButton) {
nextButton.addEventListener('click', function() { nextButton.addEventListener('click', function() {

View File

@@ -0,0 +1,306 @@
{% 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>
<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 %}
</div>
</div>
{% endif %}
{% 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 %}

View File

@@ -1,9 +1,10 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %} {% load static %}
{% block content %} {% block content %}
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">
<div class="max-w-md mx-auto bg-white rounded-lg shadow-lg p-6"> <div class="max-w-md mx-auto bg-white rounded-lg shadow-lg p-6 dark:bg-transparent dark:border-2 dark:text-white">
<h1 class="text-2xl font-bold mb-6 text-center text-blue-600">Spieler einrichten</h1> <h1 class="text-2xl font-bold mb-6 text-center text-blue-600">Spieler einrichten</h1>
<div class="text-center mb-6"> <div class="text-center mb-6">
@@ -17,20 +18,20 @@
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="join_code" value="{{join_code}}"> <input type="hidden" name="join_code" value="{{join_code}}">
<div class="bg-gray-50 p-4 rounded-lg mb-4"> <div class="bg-gray-50 p-4 rounded-lg mb-4 dark:bg-transparent">
<p class="text-sm text-gray-600 mb-2">Wähle einen Anzeigenamen für das Spiel:</p> <p class="text-sm text-gray-600 mb-2 dark:text-white">Wähle einen Anzeigenamen für das Spiel:</p>
<input type="text" <input type="text"
name="display_name" name="display_name"
required required
maxlength="20" maxlength="20"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Dein Spielername"> placeholder="Dein Spielername">
<p class="text-xs text-gray-500 mt-2">Maximal 20 Zeichen</p> <p class="text-xs text-gray-500 mt-2 dark:text-white">Maximal 20 Zeichen</p>
</div> </div>
<div class="flex justify-center space-x-4"> <div class="flex justify-center space-x-4">
<a href="/" <a href="/"
class="px-6 py-2 bg-gray-200 hover:bg-gray-300 rounded-lg transition-colors duration-200"> class="px-6 py-2 bg-gray-200 hover:bg-gray-300 rounded-lg transition-colors duration-200 dark:bg-transparent dark:hover:bg-[#2a2f3a]">
Abbrechen Abbrechen
</a> </a>
<button type="submit" <button type="submit"

View File

@@ -1,13 +1,13 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
<div class="sm:grid sm:place-items-center sm:min-h-[calc(100vh-5rem)] flex justify-center py-6 m-2"> <div class="grid place-items-center min-h-[calc(100vh-5rem)] flex justify-center py-6 m-2">
<div class="max-w-md w-full p-8 rounded-2xl border-2 border-blue-400 bg-white"> <div class="max-w-md w-full p-8 rounded-2xl border-2 border-blue-400 bg-white dark:bg-transparent dark:text-white">
<h1 class="text-2xl text-center font-bold mb-4">Spiel beitreten</h1> <h1 class="text-2xl text-center font-bold mb-4 ">Spiel beitreten</h1>
<form action="{% url 'play:join_game' %}" method="post" class="flex flex-col gap-4"> <form action="{% url 'play:join_game' %}" method="post" class="flex flex-col gap-4">
{% csrf_token %} {% csrf_token %}
<div class="flex gap-3 items-center justify-center"> <div class="flex gap-3 items-center justify-center">
<input type="number" name="game_code" placeholder="123456" maxlength="6" class="w-32 p-3 rounded-full bg-gray-200 focus:scale-105 transition duration-200 font-black focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100 text-center tracking-wider" autofocus> <input type="number" name="game_code" placeholder="123456" maxlength="6" class="w-32 p-3 rounded-full bg-gray-200 focus:scale-105 transition duration-200 font-black focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100 text-center tracking-wider text-black" autofocus>
<button type="submit" class="h-12 px-6 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-700 active:scale-95 group" disabled> <button type="submit" class="h-12 px-6 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-700 active:scale-95 group" disabled>
<span class="hidden sm:block">Beitreten</span> <span class="hidden sm:block">Beitreten</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 transition-transform group-hover:translate-x-1"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 transition-transform group-hover:translate-x-1">

View File

@@ -3,18 +3,18 @@
{% block content %} {% block content %}
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">
<div class="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-6"> <div class="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-6 dark:bg-transparent dark:text-white">
<!-- Quiz Info --> <!-- Quiz Info -->
<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 dark:text-white">{{ quiz.name }}</h3>
{% if request.session.mode != "learn" %} {% if request.session.mode != "learn" %}
{% if host_id %} {% 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 dark:bg-transparent">
<h1 class="text-4xl font-bold text-blue-800">{{ quiz_game.join_code }}</h1> <h1 class="text-4xl font-bold text-blue-800 dark:text-white">{{ quiz_game.join_code }}</h1>
<div class="w-full flex justify-center my-2"> <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"> <img src="data:image/png;base64,{{ qr_code_base64 }}" class="h-1/2 w-1/2" alt="QR-Code">
</div> </div>
<p class="text-sm text-blue-600 mt-1">Spiel-Code</p> <p class="text-sm text-blue-600 mt-1 dark:text-white">Spiel-Code</p>
</div> </div>
</div> </div>
{% endif %} {% endif %}
@@ -23,23 +23,23 @@
<!-- Participant Info --> <!-- Participant Info -->
{% if participant %} {% if participant %}
<div class="mb-6 text-center"> <div class="mb-6 text-center">
<div class="inline-flex items-center bg-green-100 px-4 py-2 rounded-full"> <div class="inline-flex items-center bg-green-100 px-4 py-2 rounded-full dark:text-black">
<span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span> <span class="w-2 h-2 bg-green-500 rounded-full mr-2 "></span>
<span>Angemeldet als <b>{{ participant.display_name }}</b></span> <span>Angemeldet als <b>{{ participant.display_name }}</b></span>
</div> </div>
</div> </div>
{% endif %} {% endif %}
<!-- Participants List --> <!-- Participants List -->
<div class="mb-6 overflow-y-scroll max-h-96"> <div class="mb-6 overflow-y-scroll max-h-96 overflow-x-hidden">
<h3 class="font-bold text-lg mb-3 flex items-center"> <h3 class="font-bold text-lg mb-3 flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg> </svg>
Teilnehmer Teilnehmer
</h3> </h3>
<ul id="participants-list" class="space-y-2 max-h-60"> <ul id="participants-list" class="space-y-2 max-h-60 ">
<li class="text-gray-500 p-3 bg-gray-50 rounded-lg text-center">Warte auf Teilnehmer...</li> <li class=" p-3 bg-gray-50 rounded-lg text-center dark:bg-transparent">Warte auf Teilnehmer...</li>
</ul> </ul>
</div> </div>
@@ -248,7 +248,7 @@
} }
if (!participants || participants.length === 0) { if (!participants || participants.length === 0) {
list.innerHTML = '<li class="text-gray-500 p-3 bg-gray-50 rounded-lg text-center">Noch keine Teilnehmer...</li>'; list.innerHTML = '<li class="text-gray-500 p-3 bg-gray-50 rounded-lg text-center dark:bg-transparent">Noch keine Teilnehmer...</li>';
return; return;
} }
@@ -298,13 +298,13 @@
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' + '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' +
'd="M6 18L18 6M6 6l12 12"/>' + 'd="M6 18L18 6M6 6l12 12"/>' +
'</svg>' + '</svg>' +
'<span class="absolute -top-8 left-1/2 transform -translate-x-1/2 px-2 py-1 ' + '<span class="absolute pr-8 -top-8 left-1/2 transform -translate-x-1/2 px-2 py-1 ' +
'bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 ' + 'bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 ' +
'transition-opacity duration-200 whitespace-nowrap">Spieler kicken</span>' + 'transition-opacity duration-200 whitespace-nowrap">Spieler kicken</span>' +
'</button>'; '</button>';
} }
return '<li class="p-3 bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition duration-200 flex items-center justify-between animate-fade-in">' + return '<li class="dark:bg-transparent p-3 bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition duration-200 flex items-center justify-between animate-fade-in">' +
'<div class="flex items-center">' + '<div class="flex items-center">' +
'<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-3">' + '<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-3">' +
'<span class="text-blue-600 font-bold">' + initial.toUpperCase() + '</span>' + '<span class="text-blue-600 font-bold">' + initial.toUpperCase() + '</span>' +

View File

@@ -4,7 +4,7 @@
<div class="grid place-content-center md:h-screen h-150 w-full -z-50 top-0 p-8 "> <div class="grid place-content-center md:h-screen h-150 w-full -z-50 top-0 p-8 ">
<div class="rounded-md rounded-xl shadow-lg border-3 border-blue-100 p-10"> <div class="rounded-md rounded-xl shadow-lg border-3 border-blue-100 p-10">
<span class="flex justify-center font-bold p-4 text-center text-1xl lg:text-2xl">Bitte wählen Sie einen Spielmodus aus:</span> <span class="flex justify-center font-bold p-4 text-center text-1xl lg:text-2xl dark:text-white">Bitte wählen Sie einen Spielmodus aus:</span>
<div class="grid sm:grid-cols-2 place-items-stretch text-center gap-4 p-4"> <div class="grid sm:grid-cols-2 place-items-stretch text-center gap-4 p-4">

View File

@@ -3,15 +3,15 @@
{% block content %} {% block content %}
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="max-w-lg mx-auto mt-10"> <div class="max-w-lg mx-auto mt-10">
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md"> <div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:text-white dark:bg-transparent">
<div class="text-center"> <div class="text-center">
<svg class="mx-auto h-12 w-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="mx-auto h-12 w-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg> </svg>
<h2 class="mt-4 text-xl font-bold text-gray-900">Löschen bestätigen</h2> <h2 class="mt-4 text-xl font-bold text-gray-900 dark:text-white">Löschen bestätigen</h2>
<p class="mt-2 text-sm text-gray-600"> <p class="mt-2 text-sm text-gray-600 dark:text-white">
Möchten Sie ihren Account wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden. Möchten Sie ihren Account wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.
</p> </p>
</div> </div>

View File

@@ -3,7 +3,7 @@
{% block title %}Passwort ändern{% endblock %} {% block title %}Passwort ändern{% endblock %}
{% block content %} {% block content %}
<div class="grid place-content-center h-120 mt-12 mb-12 "> <div class="grid place-content-center h-120 mt-12 mb-12 ">
<div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90"> <div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90 dark:text-white!">
<h2 class="text-2xl text-center p-4 font-black">Passwort ändern</h2> <h2 class="text-2xl text-center p-4 font-black">Passwort ändern</h2>
<form class="space-y-4" method="post"> <form class="space-y-4" method="post">
{% if form.errors %} {% if form.errors %}
@@ -21,17 +21,17 @@
<div class="register"> <div class="register">
{{ form.old_password.label_tag }} {{ form.old_password.label_tag }}
<input type="password" name="old_password" placeholder="ALTES PASSWORT" /> <input type="password" name="old_password" placeholder="ALTES PASSWORT" class="dark:text-black" />
</div> </div>
<div class="register"> <div class="register">
{{ form.new_password1.label_tag }} {{ form.new_password1.label_tag }}
<input type="password" name="new_password1" placeholder="NEUES PASSWORT" /> <input type="password" name="new_password1" placeholder="NEUES PASSWORT" class="dark:text-black" />
</div> </div>
<div class="register"> <div class="register">
{{ form.new_password2.label_tag }} {{ form.new_password2.label_tag }}
<input type="password" name="new_password2" placeholder="NEUES PASSWORT WIEDERHOLEN" /> <input type="password" name="new_password2" placeholder="NEUES PASSWORT WIEDERHOLEN" class="dark:text-black" />
</div> </div>
<button class="w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200" type="submit">Bestätigen</button> <button class="w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200" type="submit">Bestätigen</button>
<button class="text-center text-sm w-full mt-2 font-light text-blue-600"><a href="{% url 'accounts:home' %}" class="register_link">Doch nicht ändern?</a></button> <button class="text-center text-sm w-full mt-2 font-light text-blue-600"><a href="{% url 'accounts:home' %}" class="register_link">Doch nicht ändern?</a></button>

View File

@@ -3,7 +3,7 @@
{% block title %}Passwort zurückgesetzt{% endblock %} {% block title %}Passwort zurückgesetzt{% endblock %}
{% block content %} {% block content %}
<div class="grid place-content-center h-120 mt-12 mb-12 "> <div class="grid place-content-center h-120 mt-12 mb-12 ">
<div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90"> <div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90 dark:text-white">
<h2 class="text-2xl text-center p-4 font-black">Passwort zurückgesetzt</h2> <h2 class="text-2xl text-center p-4 font-black">Passwort zurückgesetzt</h2>
<div class="space-y-4"> <div class="space-y-4">
Das Passwort wurde erfolgreich zurückgesetzt! Sie können sich nun mit dem neuen Passwort anmelden! Das Passwort wurde erfolgreich zurückgesetzt! Sie können sich nun mit dem neuen Passwort anmelden!

View File

@@ -4,7 +4,7 @@
{% block content %} {% block content %}
<div class="grid place-content-center h-120 mt-12 mb-12 "> <div class="grid place-content-center h-120 mt-12 mb-12 ">
<div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90"> <div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90">
<h2 class="text-2xl text-center p-4 font-black">Passwort zurücksetzen</h2> <h2 class="text-2xl text-center p-4 font-black dark:text-white">Passwort zurücksetzen</h2>
{% if validlink %} {% if validlink %}
<form class="space-y-4" method="post"> <form class="space-y-4" method="post">
{% if form.errors %} {% if form.errors %}
@@ -21,11 +21,12 @@
{% csrf_token %} {% csrf_token %}
<div class="register"> <div class="register">
{{ form.new_password1.label_tag }}
<span class="dark:text-white">{{ form.new_password1.label_tag }} </span>
<input type="password" name="new_password1" placeholder="NEUES PASSWORT" /> <input type="password" name="new_password1" placeholder="NEUES PASSWORT" />
</div> </div>
<div class="register"> <div class="register ">
{{ form.new_password2.label_tag }} <span class="dark:text-white">{{ form.new_password2.label_tag }} </span>
<input type="password" name="new_password2" placeholder="NEUES PASSWORT WIEDERHOLEN" /> <input type="password" name="new_password2" placeholder="NEUES PASSWORT WIEDERHOLEN" />
</div> </div>

View File

@@ -4,12 +4,12 @@
{% block content %} {% block content %}
<div class="grid place-content-center h-120 mt-12 mb-12"> <div class="grid place-content-center h-120 mt-12 mb-12">
<div class="w-80 p-6 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90 text-center"> <div class="w-80 p-6 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90 text-center dark:text-white">
<h2 class="text-2xl font-black mb-4">E-Mail wurde gesendet</h2> <h2 class="text-2xl font-black mb-4 dark:text-white">E-Mail wurde gesendet</h2>
<p class="text-gray-700 mb-6"> <p class="text-gray-700 mb-6 dark:text-white">
Wenn ein Konto mit dieser E-Mail-Adresse existiert, haben wir dir eine Nachricht mit einem Link zum Zurücksetzen des Passworts gesendet. Wenn ein Konto mit dieser E-Mail-Adresse existiert, haben wir dir eine Nachricht mit einem Link zum Zurücksetzen des Passworts gesendet.
</p><p class="text-gray-700 mb-6 font-bold">&nbsp; Tipp: Gegebenenfalls Spam überprüfen.</p> </p><p class="text-gray-700 mb-6 font-bold dark:text-white">&nbsp; Tipp: Gegebenenfalls Spam überprüfen.</p>
<a href="{% url 'accounts:login' %}" class="block w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200">Zur Anmeldung</a> <a href="{% url 'accounts:login' %}" class="block w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200">Zur Anmeldung</a>

View File

@@ -3,7 +3,7 @@
{% block title %}Passwort ändern{% endblock %} {% block title %}Passwort ändern{% endblock %}
{% block content %} {% block content %}
<div class="grid place-content-center h-120 mt-12 mb-12 "> <div class="grid place-content-center h-120 mt-12 mb-12 ">
<div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90"> <div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90 dark:text-white">
<h2 class="text-2xl text-center p-4 font-black">Passwort zurücksetzen</h2> <h2 class="text-2xl text-center p-4 font-black">Passwort zurücksetzen</h2>
<form class="space-y-4" method="post"> <form class="space-y-4" method="post">
Gib hier deine E-Mail an und erhalte eine E-Mail, um das Passwort zurückzusetzen. Gib hier deine E-Mail an und erhalte eine E-Mail, um das Passwort zurückzusetzen.
@@ -23,7 +23,7 @@
<div class="register"> <div class="register">
{{ form.email.label_tag }} {{ form.email.label_tag }}
<input type="email" name="email" placeholder="E-MAIL" /> <input type="email" name="email" placeholder="E-MAIL" class="dark:text-black" />
</div> </div>

View File

@@ -9,3 +9,8 @@ redis
channels_redis channels_redis
python-dotenv # Production server safety python-dotenv # Production server safety
qrcode~=7.4.2 qrcode~=7.4.2
reportlab~=4.2.0
pip-licenses ~= 5.0.0
requests
requests
geopy