Compare commits

..

176 Commits

Author SHA1 Message Date
ben8
d7f4afb6f4 Quiz_updatet_date 2025-06-29 16:42:54 +02:00
ben8
73345bf6b2 fix_zu published_at 2025-06-29 16:27:32 +02:00
ben8
6c7e4770d6 fix_published_date_bei_Kopie 2025-06-29 16:26:20 +02:00
ben8
0fe5c0cac3 credits 2025-06-27 16:29:00 +02:00
ben8
c912b63d8d neuer Slogan 2025-06-27 16:18:36 +02:00
ben8
177c200467 sinnvolle Sortierung 2025-06-27 15:53:46 +02:00
ben8
2a06add37e fix 2025-06-27 15:37:45 +02:00
ben8
fd3464711b Merge branch '143-zeitangabe-fur-quizze' into 'master'
Resolve "Zeitangabe für Quizze"

Closes #143

See merge request enrichment-2024/qivip!98
2025-06-27 13:19:14 +00:00
ben8
93e166ece0 Bewertung_fix und Veröffentlichungszeit 2025-06-27 15:17:27 +02:00
juhnsa
096349c989 Standardmaessig 4 statt 2 Optionen bei Multiple Choice Fragen 2025-06-27 13:47:14 +02:00
ben8
1014ea37ec Merge branch '140-umstrukturierung-der-library' into 'master'
Resolve "Umstrukturierung der Library"

Closes #140

See merge request enrichment-2024/qivip!97
2025-06-23 12:55:20 +00:00
ben8
e65cd408eb improved_library_structure#140 2025-06-23 14:54:46 +02:00
ben8
d2274960cb richtige Anzeige der Punkte fix zu #139 2025-06-14 15:21:29 +02:00
ben8
016c253bc4 Merge branch '139-frage-bei-doppelklick-wird-falschlich-ubersprungen' into 'master'
Resolve "Frage bei Doppelklick wird fälschlich übersprungen!"

Closes #139

See merge request enrichment-2024/qivip!96
2025-06-14 13:15:18 +00:00
ben8
9f0d80bc41 weiterButton Fragen-richtig überspringen und #139 (gelöst) 2025-06-14 15:14:54 +02:00
ben8
f8dad9f0bd Merge branch '138-zahlen-bei-anmeldung-wie-viele-quizze-fragen-erstellt' into 'master'
Resolve "Zahlen bei Anmeldung -- wie viele Quizze, Fragen erstellt"

Closes #138

See merge request enrichment-2024/qivip!95
2025-06-14 12:24:30 +00:00
ben8
d55c4babc0 Erfolge#138 2025-06-14 14:24:05 +02:00
ben8
4875ed9676 Merge branch '137-credits-auch-fur-fragen' into 'master'
Resolve "Credits auch für Fragen"

Closes #137

See merge request enrichment-2024/qivip!94
2025-06-13 18:36:48 +00:00
ben8
7fcad195bd Credits bei Fragen 2025-06-13 20:36:06 +02:00
ben8
78fbdc9cc6 Credits-bei Fragen 2025-06-13 20:35:51 +02:00
ben8
4e399a5423 Credits-verbessert (Link und Darstellung) 2025-06-13 18:04:44 +02:00
ben8
c56d850dfa Merge branch '90-404-seite-desinen-2' into 'master'
Resolve "404 Seite designen"

Closes #90

See merge request enrichment-2024/qivip!40
2025-06-13 15:55:29 +00:00
ben8
136fa7864a customized-404-Seite#90 2025-06-13 17:53:02 +02:00
ben8
e916c010fe Merge branch '136-wenn-kein-bild-uberschrift-auf-blauem-hintergrund-bei-den-quizzen' into 'master'
Resolve "Wenn kein Bild: Überschrift auf blauem Hintergrund bei den Quizzen!"

Closes #136

See merge request enrichment-2024/qivip!93
2025-06-13 15:44:13 +00:00
ben8
809a4495d5 Schrift-auf_Quiz#136 2025-06-13 17:43:55 +02:00
ben8
5ca48f9287 Merge branch '132-in-der-anzeige-wenn-man-sich-die-antwortmoglichketen-anzeigt-da-man-auf-das-auge-geklickt-hat' into 'master'
Resolve "In der Anzeige wenn man sich die Antwortmöglichketen anzeigt da man auf das Auge geklickt hat auch die Bilder darstellen!"

Closes #132

See merge request enrichment-2024/qivip!92
2025-06-13 15:30:19 +00:00
ben8
16471141b2 pictures_and_numbers(detail_quiz)#132 2025-06-13 17:29:43 +02:00
ben8
f008d932ab Merge branch '135-design' into 'master'
Resolve "Design"

Closes #135

See merge request enrichment-2024/qivip!91
2025-06-08 17:51:51 +00:00
ben8
431e1c07c8 design#135 2025-06-08 19:51:10 +02:00
ben8
c41d9cc9b5 Merge branch '131-style-optimieren' into 'master'
Resolve "Style optimieren!"

Closes #131

See merge request enrichment-2024/qivip!90
2025-06-07 17:28:37 +00:00
ben8
fec37580ed style_überarbeitet#131 2025-06-07 19:28:17 +02:00
Frank Poetzsch-Heffter
b06ed09492 Merge branch '130-anpassungen-fur-produktivserverbetrieb' into 'master'
Änderungen zum Produktivbetrieb

Closes #130

See merge request enrichment-2024/qivip!89
2025-06-03 15:40:09 +00:00
Frank Poetzsch-Heffter
1771cd0d77 Änderungen zum Produktivbetrieb 2025-06-03 17:28:51 +02:00
ben8
609a3a423a Merge branch '129-faqs-auch-in-der-db-bearbeiten-konnen' into 'master'
Resolve "Faqs auch in der DB bearbeiten können!"

Closes #129

See merge request enrichment-2024/qivip!87
2025-06-01 17:34:56 +00:00
ben8
aea07295d8 Faqs über DB hinzufügen! 2025-06-01 19:34:21 +02:00
ben8
3bfca331f2 Merge branch '127-suchfunktion-verbessern' into 'master'
Resolve "Suchfunktion verbessern"

Closes #127

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

Closes #126

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

Closes #125

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

Closes #124

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

Closes #123

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

Closes #120

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

Closes #110

See merge request enrichment-2024/qivip!76
2025-05-17 12:57:58 +00:00
nik8
43f81a5ae7 Musik 2025-05-17 14:55:41 +02:00
nik8
c66e22a436 Merge branch '121-teilnehmer-nicht-uberdecken' into 'master'
Resolve "Teilnehmer nicht überdecken"

Closes #121

See merge request enrichment-2024/qivip!78
2025-05-17 10:46:24 +00:00
nik8
fbec1fed05 Scrollbar in Lobby 2025-05-17 12:46:03 +02:00
ben8
169f9b41a1 Merge branch '119-design' into 'master'
Resolve "Design"

Closes #119

See merge request enrichment-2024/qivip!75
2025-05-17 10:11:27 +00:00
ben8
75e25fb3df fix und Balkenanimation 2025-05-17 12:08:25 +02:00
nik8
e1d4f145f0 UPDATE SLOGAN 2025-05-17 11:42:01 +02:00
7eb2a80656 Merge branch 'master' of edugit.org:enrichment-2024/qivip 2025-05-10 15:36:45 +02:00
3aaf684b0e Datenschutzhinweise in Register und New Quiz 2025-05-10 15:35:34 +02:00
ben8
9199678b4b Merge branch '94-langfristig-unnotige-packages-code-etc-entfernen' into 'master'
Resolve "Langfristig: Unnötige Packages/Code etc. entfernen!"

Closes #94

See merge request enrichment-2024/qivip!73
2025-05-10 12:41:49 +00:00
ben8
5d8f3ed820 Unnötiges zum Teil entfernt! 2025-05-10 14:41:31 +02:00
a5d4e75040 Rechtschreibfehler im FAQ 2025-05-10 14:37:26 +02:00
7e4bb05c74 Datenschutz edit 2025-05-10 14:25:58 +02:00
1bb2666784 Datenschutzerklaerung 2025-05-10 14:23:07 +02:00
nik8
c05c3fd4a0 faqs_update4 2025-05-10 13:53:55 +02:00
nik8
c9a1342f1d faqs_update3 2025-05-10 13:52:34 +02:00
nik8
991488790a faqs_update2 2025-05-10 13:44:13 +02:00
ben8
c014003062 Merge branch '114-suchleiste-vorschlage-anzeigen' into 'master'
Resolve "Suchleiste - Vorschläge anzeigen!"

Closes #114

See merge request enrichment-2024/qivip!70
2025-05-10 10:53:30 +00:00
nik8
8133eaa439 faqs_update 2025-05-10 12:17:17 +02:00
38a2ffcaff CSS Fix 2025-05-10 12:11:50 +02:00
ben8
cfa95a84e7 Merge branch '117-faqs' into 'master'
Resolve "FAQs"

Closes #117

See merge request enrichment-2024/qivip!72
2025-05-10 10:02:47 +00:00
ben8
64e89d3749 Merge branch '81-uberarbeitung-des-kontos-fur-umbenennung-und-co' into 'master'
Resolve "Überarbeitung des Kontos für Umbenennung und co."

Closes #81

See merge request enrichment-2024/qivip!65
2025-05-10 09:45:26 +00:00
ben8
2708be3177 faqs 2025-05-10 11:34:39 +02:00
ben8
647b45d8a6 Merge branch '115-fehler-eingabe-funktion' into 'master'
Resolve "Fehler Eingabe Funktion"

Closes #115

See merge request enrichment-2024/qivip!71
2025-05-09 13:54:49 +00:00
ben8
c8e2817b07 Antwort_abschicken_ENTER+Design_verbessert 2025-05-09 15:53:34 +02:00
ben8
01e7909e3a fix Eingabe 2025-05-08 21:27:21 +02:00
dfee602d54 CSS Fixes for Login / Logout Button 2025-05-07 15:26:05 +02:00
ben8
0062f88167 Suchleiste Vorschläge 2025-05-04 21:28:19 +02:00
ben8
de3d3dd00c Hinweis, wenn Url nicht mehr gültig! 2025-05-03 17:20:10 +02:00
ben8
85ba8bc9cb Merge branch '113-suchleiste-optimieren' into 'master'
Resolve "Suchleiste optimieren"

Closes #113

See merge request enrichment-2024/qivip!68
2025-05-03 10:10:35 +00:00
ben8
a8103b4a20 kein_Verspringen_der_Suchleiste fix #113 2025-05-03 12:10:06 +02:00
ben8
ba3e89f9f7 Hinweis_Quiz_zu_favoriten_hinzugefügt 2025-05-03 11:44:22 +02:00
ben8
dbc457b21d Merge branch '75-idee-neuer-spielmodus-eingabe' into 'master'
Resolve "IDEE: Neuer Spielmodus: Eingabe"

Closes #75

See merge request enrichment-2024/qivip!67
2025-05-02 17:30:06 +00:00
ben8
a6cac52ba6 Eingabe_Funktion 2025-05-02 17:06:29 +02:00
ben8
5f10c2a57b Umbenennung Funktion in Passwort vergessen? bei Login! 2025-04-30 17:08:55 +02:00
ben8
19092b092d fix_change_password 2025-04-27 12:02:53 +02:00
ben8
2ca623bf33 Passwort-zurücksetzen, Passwort-ändern, Account-löschen! 2025-04-24 12:41:36 +02:00
ben8
a7c42ecf13 Merge branch '108-bilderupload-verbessern' into 'master'
Resolve "Bilderupload verbessern!"

Closes #108

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

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

Closes #89

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

Closes #83

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

Closes #71

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

Closes #70 and #72

See merge request enrichment-2024/qivip!61
2025-04-22 15:57:55 +00:00
Juhn-Vitus Saß
01562c6ad8 Konfetti animation
Fix #72
2025-04-22 17:57:27 +02:00
ben8
50cd95daf1 Uploadfunktion! 2025-04-22 17:52:34 +02:00
ben8
1a4b9e0d6b Hübscheres_Bilder_Upload+Zurücksetzfunktion 2025-04-22 17:51:19 +02:00
Juhn-Vitus Saß
867c839202 Abhängigkeiten erneuern 2025-04-22 14:24:26 +02:00
Juhn-Vitus Saß
8d8e211c72 Merge branch '73-lobby-ueberarbeiten' into 'master'
Resolve "Lobby ueberarbeiten"

Closes #73

See merge request enrichment-2024/qivip!56
2025-04-22 10:30:35 +00:00
Juhn-Vitus Saß
6da095c35e Overflow entfernen 2025-04-22 12:29:44 +02:00
Juhn-Vitus Saß
2dc9a077ee Benachrichtigungen fixen mit Redis 2025-04-22 12:21:47 +02:00
Juhn-Vitus Saß
881bc3d6c1 Merge branch '79-filter-problem-der-filter-soll-sich-nicht-schliessen-wenn-man-auf-filtern-klickt' into 'master'
Resolve "Filter Problem! Der Filter soll sich nicht schließen wenn man auf Filtern klickt."

Closes #79

See merge request enrichment-2024/qivip!55
2025-04-22 09:47:05 +00:00
Juhn-Vitus Saß
b1e438229a Filter bleibt offen wenn man auf filtern drückt 2025-04-22 11:46:44 +02:00
Juhn-Vitus Saß
97e4fa9505 Merge branch '82-bei-dem-kopieren-wird-momentan-noch-die-bewertung-ubernommen' into 'master'
Resolve "Bei dem Kopieren wird momentan noch die Bewertung übernommen!"

Closes #82

See merge request enrichment-2024/qivip!54
2025-04-21 19:20:30 +00:00
Juhn-Vitus Saß
3981dc31da Bewertung und Anzahl an Bewertungen zurücksetzen 2025-04-21 21:20:07 +02:00
Juhn-Vitus Saß
7b86b0791c Merge branch '104-kein-rauskicken-nach-30-sekunden' into 'master'
Resolve "kein rauskicken nach 30 Sekunden"

Closes #104 and #105

See merge request enrichment-2024/qivip!53
2025-04-21 11:36:28 +00:00
Juhn-Vitus Saß
7faf931fb2 - Entfernen des automatischen Kick-Mechanismus
- Fix #105
2025-04-21 13:35:51 +02:00
Juhn-Vitus Saß
9245f928dc Fix: Error when creating question without image 2025-04-21 11:54:30 +02:00
Frank Poetzsch-Heffter
1f5a177060 typo corrected 2025-04-21 06:34:31 +00:00
Juhn-Vitus Saß
ebbe4f7193 Merge branch '23-bilder-fur-fragen-und-co' into 'master'
Resolve "Bilder für Fragen und co."

Closes #23

See merge request enrichment-2024/qivip!49
2025-04-18 19:30:15 +00:00
Juhn-Vitus Saß
acd07bfb80 Merge branch 'master' into '23-bilder-fur-fragen-und-co'
# Conflicts:
#   django/templates/play/game/question_host.html
2025-04-18 19:29:59 +00:00
Juhn-Vitus Saß
d824816be8 Bilder für Fragen 2025-04-18 21:26:40 +02:00
ben8
2c563b3471 Merge branch '107-favoritensystem-verbessern' into 'master'
Resolve "Favoritensystem verbessern"

Closes #107

See merge request enrichment-2024/qivip!52
2025-04-18 16:47:37 +00:00
ben8
4122f0631b Favorit entfernen-in_DB_löschen 2025-04-18 18:47:10 +02:00
ben8
2372cf7146 Merge branch '106-quizze-mit-null-fragen-sollen-nicht-gespielt-werden-konnen' into 'master'
Resolve "Quizze mit null Fragen sollen nicht gespielt werden können!"

Closes #106

See merge request enrichment-2024/qivip!48
2025-04-18 16:33:34 +00:00
ben8
6305af9879 Merge branch 'master' of edugit.org:enrichment-2024/qivip 2025-04-18 18:27:37 +02:00
ben8
176a3cc15c Merge branch 'master' of edugit.org:enrichment-2024/qivip 2025-04-18 18:27:27 +02:00
ben8
fa9bb45d3f Merge branch 'master' of edugit.org:enrichment-2024/qivip 2025-04-18 18:12:25 +02:00
ben8
cc449997dc Merge branch 'master' of edugit.org:enrichment-2024/qivip 2025-04-18 18:12:13 +02:00
ben8
e813660722 Merge branch 'master' of edugit.org:enrichment-2024/qivip 2025-04-18 18:11:35 +02:00
ben8
1d6fab919e Merge branch 'master' of edugit.org:enrichment-2024/qivip 2025-04-18 18:11:07 +02:00
ben8
17195a221f Merge branch 'master' of edugit.org:enrichment-2024/qivip 2025-04-18 18:10:50 +02:00
ben8
3fc08cbd4e Merge branch 'cherry-pick-d4252c12' into 'master'
Merge branch '103-verschiedene-lernmodi' into 'master'

Closes #103

See merge request enrichment-2024/qivip!51
2025-04-18 18:07:03 +02:00
ben8
4cf6f45daf Merge branch 'cherry-pick-d4252c12' into 'master'
Merge branch '103-verschiedene-lernmodi' into 'master'

Closes #103

See merge request enrichment-2024/qivip!51
2025-04-18 14:13:18 +00:00
ben8
d2ad0f041f Edit question_host.html 2025-04-18 14:11:57 +00:00
ben8
bcdfebf791 Merge branch '103-verschiedene-lernmodi' into 'master'
Resolve "verschiedene Lernmodi"

Closes #103

See merge request enrichment-2024/qivip!47

(cherry picked from commit d4252c1297)

5b4c76a1 Zwischenstand Lernmodus
4209dcbc Merge branch 'master' into 103-verschiedene-lernmodi
9fb9ab91 Lernfunktion
e22ea409 Lernmodus verbessert und Testmodus entfernt
2052a702 Merge branch 'master' into '103-verschiedene-lernmodi'

Co-authored-by: ben8 <ben_ollech@bessermailer.de>
2025-04-18 14:00:29 +00:00
ben8
937fd4a2e4 Merge branch 'revert-d4252c12' into 'master'
Revert "Merge branch '103-verschiedene-lernmodi' into 'master'"

See merge request enrichment-2024/qivip!50
2025-04-18 13:51:58 +00:00
ben8
a9343f285a Revert "Merge branch '103-verschiedene-lernmodi' into 'master'"
This reverts merge request !47
2025-04-18 13:48:23 +00:00
ben8
d4252c1297 Merge branch '103-verschiedene-lernmodi' into 'master'
Resolve "verschiedene Lernmodi"

Closes #103

See merge request enrichment-2024/qivip!47
2025-04-18 13:28:25 +00:00
ben8
2052a702f0 Merge branch 'master' into '103-verschiedene-lernmodi'
# Conflicts:
#   django/templates/play/game/question_host.html
2025-04-18 13:28:04 +00:00
ben8
a0292ce409 Startbutton-min.1Frage 2025-04-18 15:25:29 +02:00
Juhn-Vitus Saß
73cd52ea3d Merge branch '76-die-lange-der-zeit-pro-frage-anpassen' into 'master'
Resolve "Die Länge der Zeit Pro Frage anpassen."

Closes #76

See merge request enrichment-2024/qivip!36
2025-04-18 11:01:34 +00:00
Juhn-Vitus Saß
ffe636c6c7 Merge branch 'master' into '76-die-lange-der-zeit-pro-frage-anpassen'
# Conflicts:
#   django/library/forms.py
2025-04-18 11:00:52 +00:00
ben8
e22ea40910 Lernmodus verbessert und Testmodus entfernt 2025-04-18 12:48:35 +02:00
Juhn-Vitus Saß
e269264f03 Front End einbindung von Zeitauswahl 2025-04-18 12:05:50 +02:00
ben8
9fb9ab9198 Lernfunktion 2025-04-14 23:39:43 +02:00
ben8
4209dcbc2d Merge branch 'master' into 103-verschiedene-lernmodi 2025-04-14 20:26:34 +02:00
ben8
70be6396aa Merge branch '101-zwischenseite-nach-spielen-z-b-lernmodus-klassisches-moderieren' into 'master'
Resolve "Zwischenseite nach Spielen z.B. Lernmodus, klassisches Moderieren, ..."

Closes #101

See merge request enrichment-2024/qivip!45
2025-04-14 18:24:48 +00:00
ben8
5b4c76a1c8 Zwischenstand Lernmodus 2025-04-14 20:23:47 +02:00
ben8
9d9e1527f4 Merge branch '102-verbesserung-der-anti-spring-funktion-bei-library' into 'master'
Resolve "Verbesserung der "Anti-Spring" Funktion bei library!"

Closes #102

See merge request enrichment-2024/qivip!46
2025-04-14 13:12:44 +00:00
ben8
70741a5a34 #102 fix Springen 2025-04-14 15:12:16 +02:00
ben8
8022602fc5 Zwischenauswahl_Spielmodus 2025-04-14 14:44:55 +02:00
ben8
f0dcc2baad vorrübergehende_Lösung-Suchleiste 2025-04-13 18:30:45 +02:00
ben8
2221c99ad7 Merge branch '99-suchleiste-design' into 'master'
Resolve "Suchleiste design abstand"

Closes #99

See merge request enrichment-2024/qivip!43
2025-04-13 16:17:21 +00:00
ben8
d84b44252e Darstellung Suchleiste 2025-04-13 18:16:58 +02:00
ben8
5a719de8ee Merge branch '55-quiz-zu-deinen-favoriten' into 'master'
Resolve "Quiz zu deinen Favoriten"

Closes #55

See merge request enrichment-2024/qivip!39
2025-04-13 15:33:20 +00:00
ben8
7c8c142e34 Richtige Sortierung! 2025-04-13 17:31:38 +02:00
ben8
093b0a7f83 Merge branch 'master' into 55-quiz-zu-deinen-favoriten 2025-04-13 17:07:11 +02:00
ben8
a4442fe12e Merge branch '98-betreten-form-reparieren' into 'master'
Resolve "Betreten form reparieren!"

Closes #98

See merge request enrichment-2024/qivip!42
2025-04-13 15:01:52 +00:00
ben8
a59df8dd62 #98 plus besser Darstellung der Suchleiste 2025-04-13 17:01:28 +02:00
ben8
80390de6dc Merge branch '96-suchleiste-uberall-anzeigen' into 'master'
Resolve "Suchleiste überall anzeigen"

Closes #96

See merge request enrichment-2024/qivip!41
2025-04-13 11:10:27 +00:00
ben8
5f05964b1b Suchleiste_überall #96 2025-04-13 13:09:39 +02:00
ben8
7e321af48c 404 Fehler Seite-vorbereitet! 2025-04-13 12:44:12 +02:00
ben8
d53240c710 Favoriten_nur_wenn_angemeldet! 2025-04-11 20:29:55 +02:00
ben8
1f3b551b94 Favoriten 2025-04-11 19:31:12 +02:00
ben8
2a7e39bbd0 Favoritenfunktion! #55 2025-04-11 18:18:34 +02:00
ben8
04ce8104fe Fehler_behonben 2025-04-10 20:29:28 +02:00
ben8
7ce0e6e9dd Zwischenstand_Favoriten 2025-04-10 19:55:11 +02:00
ben8
77714d22a9 Zeit_bisher_im_Backend #76 2025-04-10 14:43:43 +02:00
nik8
f405016f96 zwischenstand 2025-04-09 14:22:46 +02:00
103 changed files with 5495 additions and 917 deletions

1
.gitignore vendored
View File

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

View File

@@ -7,6 +7,7 @@
3. [Superuser erstellen](#superuser-erstellen)
4. [Tailwind-Nutzung](#tailwind-nutzung)
5. [Issue - Merge Request - Merge](#issue-mergerequest-merge)
6. [Konfiguration](#konfiguration)
## Initialisierung des Projekts
@@ -40,7 +41,7 @@ Um das Projekt erfolgreich zu starten, folge diesen Schritten:
```
7. Die Datenbank migrieren:
```sh
python3 core/manage.py migrate
python django/manage.py migrate
```
## Starten des Servers
@@ -61,6 +62,13 @@ Für die Entwicklung mit WebSocket-Unterstützung verwenden wir Daphne:
daphne -b 127.0.0.1 -p 8000 core.asgi:application
```
3. Starte den REDIS Server
```sh
redis-server
```
> **Hinweis:** Der Redis Server muss, wenn er nicht auf dem selben Gerät unter Standardeinstellungen läuft, in der Datei play/consumers/lobby.py und core/settings.py entsprechend umkonfiguriert werden!
Alternativ kannst du den Django-Entwicklungsserver verwenden, wenn du keine WebSocket-Funktionalität benötigst:
```sh
python3 core/manage.py runserver
@@ -84,6 +92,19 @@ Für den Produktivbetrieb verwenden wir Daphne als ASGI-Server, der sowohl HTTP
- `-b 0.0.0.0`: Bindet den Server an alle Netzwerk-Interfaces
- `-p 8000`: Port (anpassbar)
4. Cronjob für Bereinigung der Spiele in der Datenbank
- Zum bearbeiten der Cronjobs:
```sh
crontab -e
```
- Dann `0 0 * * * /path/to/venv/bin/python /path/to/your_project/manage.py cleanup_games` einfügen, damit um Mitternacht alle Spiele gelöscht werden
5. Starte den REDIS Server
```sh
redis-server
```
> **Hinweis:** Der Redis Server muss, wenn er nicht auf dem selben Gerät unter Standardeinstellungen läuft, in der Datei play/consumers/lobby.py und core/settings.py entsprechend umkonfiguriert werden!
Damit ist das Projekt erfolgreich eingerichtet und der Server kann gestartet werden!
## Superuser erstellen
@@ -138,6 +159,14 @@ Hier der vereinbarte Arbeitsablauf:
1. Issue erstellen - das geht am einfachsten über die Webseite. Der Titel beschreibt kurz und klar, was angestrebt wird.
2. Merge Request anlegen - dadurch wird der Bransch automatisch angelegt. Dieser kann mit `git pull` geholt und mit `git checkout <branch>` bearbeitet werden.
2. Merge Request anlegen - dadurch wird der Branch automatisch angelegt. Dieser kann mit `git pull` geholt und mit `git checkout <branch>` bearbeitet werden.
3. Nach dem Hochladen kann der Merge in den Master durchgeführt werden.
## Konfiguration
Die Konfiguration des Projekts befindet sich im `config/qivip_config.json`.
| Variable | Bedeutung | Optionen |
| --- | --- | --- |
| ENABLE_RATING_SYSTEM | Soll das Bewertungssystem aktiviert werden? | true [Standard], false |

View File

@@ -0,0 +1,7 @@
Name und Anschrift des Verantwortlichen
//NAME HIER EINSETZEN//
//ADRESSE HIER EINSETZEN//
//E-MAIL HIER EINSETZEN//

View File

@@ -6,11 +6,15 @@ from django import forms
class RegisterForm(UserCreationForm):
class Meta:
model=User
fields = ['username','password1','password2']
fields = ['username','password1','password2', "email"]
username = forms.CharField(
widget=forms.TextInput(attrs={'placeholder': 'BENUTZERNAME'})
)
email = forms.EmailField(
widget=forms.TextInput(attrs={'placeholder': 'E-MAIL'})
)
password1 = forms.CharField(
widget=forms.PasswordInput(attrs={'placeholder': 'PASSWORT'})
)
@@ -19,4 +23,3 @@ class RegisterForm(UserCreationForm):
)

View File

@@ -1,7 +1,8 @@
from django.urls import path # type: ignore
from django.urls import path, reverse_lazy # type: ignore
from . import views
from django.contrib.auth.views import LogoutView
from django.contrib.auth.views import LoginView
from django.contrib.auth import views as auth_views
app_name = 'accounts' # Namensraum zum verhindern von Konflikten zwischen Apps
#
@@ -13,4 +14,22 @@ urlpatterns = [
path('logout/', LogoutView.as_view(next_page='accounts:login'), name='logout'),
path('login/', LoginView.as_view(template_name='accounts/login.html', next_page='accounts:home'), name='login'),
path('register/', views.register, name='register'),
path('password_reset/', auth_views.PasswordResetView.as_view(success_url=reverse_lazy('accounts:password_reset_done')), name='password_reset'),
path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
path('password_change/', views.password_changed, name='password_change'),
path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'),
path('delete_account/', views.deleteAccount, name="delete_account"),
path(
'reset/<uidb64>/<token>/',
auth_views.PasswordResetConfirmView.as_view(
template_name='registration/password_reset_confirm.html',
success_url=reverse_lazy('accounts:password_reset_complete')
),
name='password_reset_confirm'
),
]

View File

@@ -2,11 +2,25 @@ from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.contrib.auth import login, authenticate
from .forms import RegisterForm
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth import update_session_auth_hash
from django.contrib import messages
from django.contrib.auth import logout
from library.models import QivipQuiz, QivipQuestion
# Create your views here.
@login_required
def home(request):
return render(request, 'accounts/home.html')
my_quizzes_number=QivipQuiz.objects.filter(user_id=request.user).count()
my_questions_number = QivipQuestion.objects.filter(quiz_id__user_id=request.user).count()
context = {
'my_quizzes_number': my_quizzes_number,
'my_questions_number': my_questions_number,
}
return render(request, 'accounts/home.html', context=context)
def register(response):
if response.method == "POST":
@@ -19,3 +33,33 @@ def register(response):
form = RegisterForm()
return render(response, "accounts/register.html", {"form":form})
from django.contrib.auth import views as auth_views
@login_required
def password_changed(request):
if request.method == 'POST':
form = PasswordChangeForm(user=request.user, data=request.POST)
if form.is_valid():
user = form.save()
#update_session_auth_hash(request, user) #Hier wäre der User nach Passwortänderung noch angemeldet!
logout(request)
messages.success(request, "Das Passwort wurde erfolgreich geändert!")
return redirect('accounts:home')
else:
form = PasswordChangeForm(user=request.user)
return render(request, 'registration/password_change_form.html', {'form': form})
@login_required
def deleteAccount(request):
if request.method == 'POST':
user=request.user
logout(request)
user.delete()
messages.success(request, "Der Account wurde erfolgreich gelöscht!")
return redirect('homepage:home')
return render(request, 'registration/delete_account_confirm.html')

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -1,6 +0,0 @@
from django.apps import AppConfig
class ComponentsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'components'

View File

@@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -1,14 +0,0 @@
from django.urls import path # type: ignore
from . import views
app_name = 'components' # Namensraum zum verhindern von Konflikten zwischen Apps
#
# Wichtig: Damit Links funktionieren müssen diese so eingebunden werden: {% url 'accounts:login' %} statt {% url 'login' %}
#
urlpatterns = [
path('<int:pk>', views.play, name='play'),
path('ranking/<int:pk>', views.show_rank, name='ranking')
]

View File

@@ -1,9 +0,0 @@
from django.shortcuts import render
# Create your views here.
def play(request,pk): #
return render(request, 'components/answer.html', {"pk": pk}) #
def show_rank(request,pk): #
return render(request, 'components/ranking.html', {"pk": pk}) #

View File

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

View File

@@ -10,30 +10,42 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
import json, os
from pathlib import Path
from dotenv import load_dotenv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' #TODO: NUR FÜR ENTWICKLUNG
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# Support env variables from .env file if defined
env_path = load_dotenv(os.path.join(BASE_DIR, '.env'))
load_dotenv(env_path)
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-#+s307$rxkts=8@+_^i)8)n1b4*y4za1xz!mvv(h@!+n9!mp9)'
#SECRET_KEY = 'django-insecure-#+s307$rxkts=8@+_^i)8)n1b4*y4za1xz!mvv(h@!+n9!mp9)'
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'django-insecure-&psk#na5l=p3q8_a+-$4w1f^lt3lx1c@d*p4x$ymm_rn7pwb87')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
#DEBUG = False
#DEBUG = os.environ.get('DJANGO_DEBUG', '') != 'False'
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '*']
# Konfigurationsdatei für Projekt
with open(BASE_DIR / 'config' / 'qivip_config.json') as config_file:
QIVIP_CONFIG = json.load(config_file)
# Application definition
INSTALLED_APPS = [
'library.apps.LibraryConfig',
'homepage.apps.HomepageConfig',
'components.apps.ComponentsConfig',
'accounts.apps.AccountsConfig',
'play',
'django.contrib.admin',
@@ -43,6 +55,8 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
'django_cleanup',
'django.contrib.humanize',
]
MIDDLEWARE = [
@@ -78,12 +92,16 @@ WSGI_APPLICATION = 'core.wsgi.application'
ASGI_APPLICATION = 'core.asgi.application'
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer'
}
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6380)],
},
},
}
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
@@ -143,7 +161,7 @@ STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_DIRS = [BASE_DIR / 'static']
# WhiteNoise configuration
STATICFILES_STORAGE = 'whitenoise.storage.CompressedStaticFilesStorage'
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
WHITENOISE_SKIP_COMPRESS_EXTENSIONS = ['css', 'js']
# Return 404 instead of 500 for missing files
WHITENOISE_MISSING_FILE_ERRNO = None
@@ -158,3 +176,13 @@ import os
MEDIA_URL = '/media/' # URL, um auf Medien-Dateien zuzugreifen
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # Speicherort für hochgeladene Dateien
# Email Settings
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'postoffice.katharineum.de'
EMAIL_PORT = 587 # 25
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'qivip-mail@katharineum.de'
EMAIL_HOST_PASSWORD = '<password>'
EMAIL_USE_SSL = False
DEFAULT_FROM_EMAIL = 'qivip-mail@katharineum.de'

View File

@@ -25,7 +25,6 @@ urlpatterns = [
path('', include('homepage.urls')),
path('play/', include('play.urls')),
path('library/', include('library.urls')),
path('components/', include('components.urls'))
]
if settings.DEBUG:

View File

@@ -1,3 +1,8 @@
from django.contrib import admin
from .models import QivipFaq
# Register your models here.
class QivipFaqAdmin(admin.ModelAdmin):
list_display = ('question','answer',)
# Registrierung der Modelle im Admin
admin.site.register(QivipFaq, QivipFaqAdmin)

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.1.7 on 2025-06-01 17:16
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='QivipFAQs',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('question', models.CharField(max_length=200)),
('answer', models.CharField(max_length=1000)),
],
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.1.7 on 2025-06-01 17:30
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('homepage', '0001_initial'),
]
operations = [
migrations.RenameModel(
old_name='QivipFAQs',
new_name='QivipFaq',
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.1.7 on 2025-06-01 17:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('homepage', '0002_rename_qivipfaqs_qivipfaq'),
]
operations = [
migrations.AlterField(
model_name='qivipfaq',
name='answer',
field=models.TextField(max_length=1000),
),
migrations.AlterField(
model_name='qivipfaq',
name='question',
field=models.TextField(max_length=200),
),
]

View File

@@ -1,3 +1,6 @@
from django.db import models
# Create your models here.
class QivipFaq(models.Model):
question = models.TextField(max_length=200)
answer = models.TextField(max_length=1000)

View File

@@ -1,7 +1,10 @@
from django.shortcuts import render, redirect
from .models import QivipFaq
def home(request):
return render(request, 'homepage/home.html')
faqs=QivipFaq.objects.all()
context={"faqs": faqs}
return render(request, 'homepage/home.html', context)
def impress(request):
return render(request, 'homepage/impress.html')

View File

@@ -1,14 +1,14 @@
from django.contrib import admin
from .models import QivipQuiz, QivipQuestion, QuizCategory
from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite, QuestionImage, QuizCategory, QuizImage, QuizRating
# Für das QivipQuiz Modell
class QivipQuizAdmin(admin.ModelAdmin):
list_display = ('name', 'user_id', 'status', 'category', 'creation_date', 'update_date') # Welche Felder sollen in der Übersicht angezeigt werden
list_display = ('name','published_at', 'user_id', 'status', 'category', 'creation_date', 'update_date') # Welche Felder sollen in der Übersicht angezeigt werden
search_fields = ('name', 'user_id__username', 'category__name') # Suchfelder
list_filter = ('status', 'category') # Filteroptionen
# Für das QivipQuestion Modell
class QivipQuestionAdmin(admin.ModelAdmin):
list_display = ('id', 'quiz_id', 'data', 'creation_date', 'update_date')
list_display = ('id', 'quiz_id', 'data', 'creation_date', 'update_date', 'credits')
search_fields = ('data',)
list_filter = ('quiz_id',)
@@ -18,13 +18,22 @@ class QuizCategoryAdmin(admin.ModelAdmin):
search_fields = ('name',)
prepopulated_fields = {'slug': ('name',)} # Erstelle automatisch den Slug basierend auf dem Namen
# Für das Tag Modell
class TagAdmin(admin.ModelAdmin):
list_display = ('name',) # Zeige nur den Namen des Tags
search_fields = ('name',)
class QivipQuizFavoriteAdmin(admin.ModelAdmin):
list_display = ('user', 'favorite', 'quiz') # Welche Felder sollen in der Übersicht angezeigt werden
class QuizImageAdmin(admin.ModelAdmin):
list_display = ('image',)
class QuestionImageAdmin(admin.ModelAdmin):
list_display = ('image',)
# Registrierung der Modelle im Admin
admin.site.register(QivipQuiz, QivipQuizAdmin)
admin.site.register(QivipQuestion, QivipQuestionAdmin)
admin.site.register(QuizCategory, QuizCategoryAdmin)
admin.site.register(QivipQuizFavorite, QivipQuizFavoriteAdmin)
admin.site.register(QuizImage, QuizImageAdmin)
admin.site.register(QuestionImage, QuestionImageAdmin)

View File

@@ -1,31 +1,38 @@
from django import forms
from .models import QivipQuiz, QivipQuestion
from .models import QivipQuiz, QivipQuestion, QuizCategory
class QuizForm(forms.ModelForm):
class Meta:
model = QivipQuiz
fields = ['name', 'description','image', 'status', 'category', "difficulty","credits"]
fields = ['name', 'description', 'status', 'category','difficulty','credits']
class QuestionForm(forms.ModelForm):
class Meta:
model = QivipQuestion
fields = ['quiz_id', 'data']
fields = ['quiz_id', 'data','time_per_question']
from django import forms
class QuizFilterForm(forms.Form):
search = forms.CharField(required=False, label="Suche", widget=forms.TextInput(attrs={
'class': 'flex flex-grow rounded-lg p-2' ,'placeholder': 'Suche ...',
'class': 'flex flex-grow rounded-lg p-2' ,'placeholder': 'Suche ...', 'list': 'quiz-names',
}))
min_amout_questions = forms.IntegerField(required=False, min_value=1, label="Minimale Anzahl an Fragen", widget=forms.NumberInput(attrs={
min_amount_questions = forms.IntegerField(required=False, min_value=1, label="Minimale Anzahl an Fragen", widget=forms.NumberInput(attrs={
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full'
}))
max_amout_questions = forms.IntegerField(required=False, min_value=1, label="Maximale Anzahl an Fragen", widget=forms.NumberInput(attrs={
max_amount_questions = forms.IntegerField(required=False, min_value=1, label="Maximale Anzahl an Fragen", widget=forms.NumberInput(attrs={
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full'
}))
user = forms.CharField(required=False, label="von User", widget=forms.TextInput(attrs={
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full'
}))
categories = forms.ModelChoiceField(
queryset=QuizCategory.objects.filter().distinct(),
required=False,
label="Kategorie",
widget=forms.Select(attrs={
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full'
}))

View File

@@ -0,0 +1,13 @@
from django.core.management.base import BaseCommand
from play.models import QuizGame
from django.utils import timezone
class Command(BaseCommand):
help = 'Bereinigt beendete Spiele'
def handle(self, *args, **kwargs):
expired_games = QuizGame.objects.filter(
updated_at__lt=timezone.now() - timezone.timedelta(minutes=5))
deleted_count = expired_games.count()
expired_games.delete()
self.stdout.write(f'{deleted_count} alte Spiele gelöscht.')

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-08 14:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0032_qivipquiz_average_rating_qivipquiz_rating_count_and_more'),
]
operations = [
migrations.AddField(
model_name='qivipquestion',
name='time_per_question',
field=models.CharField(choices=[('10', 10), ('15', 15), ('30', 30), ('60', 60), ('90', 90), ('120', 120), ('180', 180), ('nicht gesetzt', 'nicht gesetzt')], default=30, help_text='Zeit pro Frage in Sekunden', max_length=13),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-08 14:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0033_qivipquestion_time_per_question'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='time_per_question',
field=models.CharField(choices=[('10', 10), ('15', 15), ('30', 30), ('60', 60), ('90', 90), ('120', 120), ('180', 180)], default=30, help_text='Zeit pro Frage in Sekunden', max_length=13),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-08 14:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0034_alter_qivipquestion_time_per_question'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='time_per_question',
field=models.CharField(choices=[('10', 10), ('15', 15), ('30', 30), ('60', 60), ('90', 90), ('120', 120), ('180', 180)], default=30, help_text='Zeit pro Frage in Sekunden', max_length=3),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-10 14:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0034_alter_qivipquiz_status'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='favorite',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,32 @@
# Generated by Django 5.1.7 on 2025-04-10 16:02
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0035_qivipquiz_favorite'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='qivipquiz',
name='favorite',
),
migrations.CreateModel(
name='QuizFavorite',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('favorite', models.BooleanField(default=False)),
('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.qivipquiz')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'quiz')},
},
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.1.7 on 2025-04-10 16:05
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0036_remove_qivipquiz_favorite_quizfavorite'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RenameModel(
old_name='QuizFavorite',
new_name='QivipQuizFavorite',
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.1.7 on 2025-04-10 17:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0037_rename_quizfavorite_qivipquizfavorite'),
]
operations = [
migrations.AlterField(
model_name='qivipquizfavorite',
name='quiz',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to='library.qivipquiz'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-11 12:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0038_alter_qivipquizfavorite_quiz'),
]
operations = [
migrations.AddField(
model_name='qivipquizfavorite',
name='favorite_date',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.1.7 on 2025-04-11 13:15
import library.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0039_qivipquizfavorite_favorite_date'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='description',
field=models.TextField(default='In dem Quiz geht es um ...', max_length=200, validators=[library.models.QivipQuiz.validate_description], verbose_name='Beschreibung'),
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 5.1.7 on 2025-04-11 13:21
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0040_alter_qivipquiz_description'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quiz', to='library.quizcategory', verbose_name='Kategorie'),
),
migrations.AlterField(
model_name='qivipquiz',
name='difficulty',
field=models.CharField(choices=[('1', '1'), ('2', '2'), ('3', '3'), ('4', '4'), ('5', '5'), ('nicht gesetzt', 'nicht gesetzt')], default='nicht gesetzt', help_text='1: niedrigste Schwierigkeit und 5: höchste Schwierigkeit', max_length=13, verbose_name='Schwierigkeit'),
),
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/', verbose_name='Bild'),
),
migrations.AlterField(
model_name='qivipquiz',
name='name',
field=models.CharField(max_length=75, verbose_name='Name des Quizzes'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.1.7 on 2025-04-11 13:43
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0041_alter_qivipquiz_category_alter_qivipquiz_difficulty_and_more'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='favorite',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quizzes', to='library.qivipquizfavorite'),
),
]

View File

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

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.1.7 on 2025-04-11 16:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0043_remove_qivipquiz_favorite'),
]
operations = [
migrations.RemoveField(
model_name='qivipquizfavorite',
name='favorite_date',
),
migrations.AddField(
model_name='qivipquiz',
name='favorite_date',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.1.7 on 2025-04-11 16:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0044_remove_qivipquizfavorite_favorite_date_and_more'),
]
operations = [
migrations.RemoveField(
model_name='qivipquiz',
name='favorite_date',
),
migrations.AddField(
model_name='qivipquizfavorite',
name='favorite_date',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-11 16:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0045_remove_qivipquiz_favorite_date_and_more'),
]
operations = [
migrations.AlterField(
model_name='qivipquizfavorite',
name='favorite_date',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,14 @@
# Generated by Django 5.1.7 on 2025-04-18 11:34
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0035_alter_qivipquestion_time_per_question'),
('library', '0046_alter_qivipquizfavorite_favorite_date'),
]
operations = [
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-18 11:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0047_merge_20250418_1334'),
]
operations = [
migrations.AddField(
model_name='qivipquestion',
name='image',
field=models.ImageField(blank=True, height_field=900, max_length=200, null=True, upload_to='question_images/', verbose_name='Bild', width_field=1000),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-18 19:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0048_qivipquestion_image'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='question_images/', verbose_name='Bild'),
),
]

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-06-13 16:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0053_questionimage_quizimage_alter_qivipquestion_image_and_more'),
]
operations = [
migrations.AddField(
model_name='qivipquestion',
name='credits',
field=models.TextField(blank=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-06-26 16:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0054_qivipquestion_credits'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='published_at',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.1.7 on 2025-06-27 12:35
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0055_qivipquiz_published_at'),
]
operations = [
migrations.AlterUniqueTogether(
name='quizrating',
unique_together=set(),
),
]

View File

@@ -3,6 +3,15 @@ from django.contrib.auth.models import User
from django.utils.text import slugify
import uuid
from django.core.exceptions import ValidationError
from django.utils import timezone
class QuizImage(models.Model):
image = models.ImageField(max_length=256,verbose_name="Bild", upload_to='quiz_images/', blank=True, null=True) # Bild speichern in media/quiz_images/
class QuestionImage(models.Model):
image = models.ImageField(verbose_name="Bild", upload_to="question_images/", blank=True, null=True)
class QivipQuiz(models.Model):
STATUS_VALUES = {
@@ -20,11 +29,22 @@ class QivipQuiz(models.Model):
}
def validate_description(value):
if value.strip() == "In dem Quiz geht es um ...":
raise ValidationError("Bitte gib eine aussagekräftige Beschreibung ein.")
image = models.ImageField(max_length=256, upload_to='quiz_images/', blank=True, null=True) # Bild speichern in media/quiz_images/
image = models.ForeignKey(
QuizImage,
null=True,
blank=True,
on_delete=models.PROTECT,
related_name="quizzes"
)
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
user_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='quiz')
#creator = models.ForeignKey(User, on_delete=models.SET_NULL,blank=True, null=True, related_name='creator_quiz')
@@ -32,26 +52,57 @@ class QivipQuiz(models.Model):
creation_date = models.DateTimeField(auto_now_add=True)
update_date = models.DateTimeField(auto_now=True)
status = models.CharField(max_length=10, choices=list(STATUS_VALUES.items()), default="öffentlich")
category = models.ForeignKey('QuizCategory', related_name='quiz', on_delete=models.CASCADE)
name = models.CharField(max_length=75)
description = models.TextField(validators=[validate_description],max_length=200,blank=False, default="In dem Quiz geht es um ...")
difficulty= models.CharField(max_length=13, choices=list(DIFFICULTY_VALUES.items()), default="nicht gesetzt", help_text="1: niedrigste Schwierigkeit und 5: höchste Schwierigkeit")
category = models.ForeignKey('QuizCategory',verbose_name="Kategorie", related_name='quiz', on_delete=models.CASCADE)
name = models.CharField(max_length=75,verbose_name="Name des Quizzes")
description = models.TextField(validators=[validate_description],max_length=200,blank=False, default="In dem Quiz geht es um ...", verbose_name="Beschreibung")
difficulty= models.CharField(verbose_name="Schwierigkeit", max_length=13, choices=list(DIFFICULTY_VALUES.items()), default="nicht gesetzt", help_text="1: niedrigste Schwierigkeit und 5: höchste Schwierigkeit")
credits=models.TextField(blank=True, editable=True)
average_rating = models.FloatField(default=3.0)
rating_count = models.IntegerField(default=0)
published_at = models.DateTimeField(null=True, blank=True) # manuell gesetzt, wenn veröffentlicht
def is_published(self):
return self.published_at is not None and self.published_at <= timezone.now()
def __str__(self):
return self.name
class QivipQuizFavorite(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) # Verknüpfung zu User
quiz = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='favorites') # Verknüpfung zu Quiz
favorite = models.BooleanField(default=False) # Favoritenstatus
favorite_date = models.DateTimeField(null=True, blank=True)
class Meta:
unique_together = ('user', 'quiz') # Sicherstellen, dass es nur ein Favorit pro User und Quiz gibt
# Create your models here.
class QivipQuestion(models.Model):
TIME_VALUES = {
"10": 10,
"15": 15,
"30": 30,
"60": 60,
"90": 90,
"120": 120,
"180": 180,
}
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
quiz_id = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='questions')
creation_date = models.DateTimeField(auto_now_add=True)
update_date = models.DateTimeField(auto_now=True)
data = models.TextField()
time_per_question = models.CharField(max_length=3, choices=list(TIME_VALUES.items()), default=30, help_text="Zeit pro Frage in Sekunden")
image = models.ForeignKey(
QuestionImage,
null=True,
blank=True,
on_delete=models.PROTECT,
related_name="questions"
)
credits=models.TextField(blank=True, editable=True)
def __str__(self):
return self.data[:50]
@@ -64,9 +115,22 @@ class QuizRating(models.Model):
rating = models.IntegerField(choices=[(i, str(i)) for i in range(1, 6)])
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('quiz', 'participant_id')
#class Meta:
#unique_together = ('quiz', 'participant_id')
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# Update quiz average rating (immer, auch bei Updates)
quiz = self.quiz
total_ratings = quiz.ratings.count()
avg_rating = quiz.ratings.aggregate(models.Avg('rating'))['rating__avg']
quiz.average_rating = round(avg_rating, 1) if avg_rating else 3.0
quiz.rating_count = total_ratings
quiz.save()
"""
def save(self, *args, **kwargs):
is_new = self.pk is None
super().save(*args, **kwargs)
@@ -78,7 +142,7 @@ class QuizRating(models.Model):
avg_rating = quiz.ratings.aggregate(models.Avg('rating'))['rating__avg']
quiz.average_rating = round(avg_rating, 1) if avg_rating else 3.0
quiz.rating_count = total_ratings
quiz.save()
quiz.save()"""
class QuizCategory(models.Model):
name = models.CharField(max_length=100, unique=True)

View File

@@ -7,6 +7,9 @@ app_name = 'library'
urlpatterns = [
path('', views.overview_quiz, name='overview_quiz'),
path('my/', views.overview_quiz_my, name='overview_quiz_my'),
path('favorites/', views.overview_quiz_favorites, name='overview_quiz_favorites'),
path('new/', views.new_quiz, name='new_quiz'),
path('edit/<int:pk>/', views.edit_quiz, name='edit_quiz'),
path('delete/<int:pk>/', views.delete_quiz, name='delete_quiz'),
@@ -15,5 +18,7 @@ urlpatterns = [
path('question/edit/<int:pk>/', views.edit_question, name='edit_question'),
path('question/delete/<int:pk>/', views.delete_question, name='delete_question'),
path('detail/copy/<int:pk>/', views.copy_quiz, name='copy_quiz'),
path('favorite_quiz/<int:pk>/', views.favorite_quiz, name='favorite_quiz'),
path("quiz-names-json/", views.quiz_names_json, name="quiz_names_json"),
]

View File

@@ -1,10 +1,12 @@
from django.utils import timezone
import uuid
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.urls import reverse
from django.contrib import messages
from .models import QivipQuiz, QivipQuestion
from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite, QuestionImage, QuizImage
from .forms import QuizForm, QuestionForm
import json
from django.core.paginator import Paginator
@@ -12,57 +14,255 @@ from .models import QivipQuiz
from .forms import QuizFilterForm
from django.db.models import Count
from django.contrib.auth.models import User
from urllib.parse import urlparse, urlunparse
# Übersicht aller Quizze
def overview_quiz(request):
# Filter
def quiz_names_json(request):
form = QuizFilterForm(request.GET)
# Initialize querysets
if request.user.is_authenticated:
filter_my_quizzes = QivipQuiz.objects.filter(user_id=request.user).annotate(max_amout_questions=Count('questions'))
filter_other_quizzes = QivipQuiz.objects.exclude(user_id=request.user)
else:
filter_my_quizzes = QivipQuiz.objects.none()
filter_other_quizzes = QivipQuiz.objects.all()
# Apply common filters to other quizzes
filter_other_quizzes = filter_other_quizzes.annotate(max_amout_questions=Count('questions'))
filter_other_quizzes = filter_other_quizzes.filter(max_amout_questions__gte=1, status='öffentlich')
if form.is_valid():
search = form.cleaned_data.get('search')
username= form.cleaned_data.get('user')
max_amout_questions= form.cleaned_data.get('max_amout_questions')
min_amout_questions= form.cleaned_data.get('min_amout_questions')
if search: # Suche nach Namen
filter_other_quizzes = filter_other_quizzes.filter(name__icontains=search)
filter_my_quizzes = filter_my_quizzes.filter(name__icontains=search)
if min_amout_questions:
filter_other_quizzes = filter_other_quizzes.filter(max_amout_questions__gte=min_amout_questions)
filter_my_quizzes = filter_my_quizzes.filter(max_amout_questions__gte=min_amout_questions) if filter_my_quizzes else QivipQuiz.objects.none()
if max_amout_questions:
filter_other_quizzes = filter_other_quizzes.filter(max_amout_questions__lte=max_amout_questions)
try:
filter_my_quizzes =filter_my_quizzes.filter(max_amout_questions__lte=max_amout_questions)
except:
filter_my_quizzes = QivipQuiz.objects.none()
names = QivipQuiz.objects.all().annotate(max_amount_questions=Count('questions')).filter(max_amount_questions__gte=1, status='öffentlich', name__icontains=search)
if request.user.is_authenticated:
names = names | QivipQuiz.objects.filter(
user_id=request.user.id,
name__icontains=search
)
names=list(names.values_list('name', flat=True).distinct())
return JsonResponse(names, safe=False)
def apply_quiz_filters(queryset, form):
if not form.is_valid():
return queryset
queryset = queryset.annotate(question_count=Count('questions'))
search = form.cleaned_data.get('search')
username = form.cleaned_data.get('user')
min_questions = form.cleaned_data.get('min_amount_questions')
max_questions = form.cleaned_data.get('max_amount_questions')
category = form.cleaned_data.get('categories')
filters = {}
if search:
filters['name__icontains'] = search
if min_questions is not None:
filters['question_count__gte'] = min_questions
if max_questions is not None:
filters['question_count__lte'] = max_questions
if category is not None:
filters['category'] = category
if username:
try:
user = User.objects.get(username=username) # Benutzer anhand des Namens holen
filter_other_quizzes = filter_other_quizzes.filter(user_id=user.id)
filter_my_quizzes = filter_my_quizzes.filter(user_id=user.id) # Nach der user_id filtern
user = User.objects.get(username=username)
filters['user_id'] = user.id
except User.DoesNotExist:
filter_other_quizzes = QivipQuiz.objects.none() # Falls User nicht existiert, leere Query zurückgeben
return queryset.none()
return queryset.filter(**filters)
def overview_quiz(request, connect="all"):
form = QuizFilterForm(request.GET)
user = request.user if request.user.is_authenticated else None
# Meine Quizzeze mit mind. 1 Frage
filter_my_quizzes = QivipQuiz.objects.none()
if user:
filter_my_quizzes = QivipQuiz.objects.annotate(max_amount_questions=Count('questions')).filter(
user_id=user.id,
max_amount_questions__gte=0
)
# Öffentliche Alle mit mind. 1 Frage
filter_all_quizzes = QivipQuiz.objects.annotate(max_amount_questions=Count('questions')).filter(
status='öffentlich',
max_amount_questions__gte=1
)
if user:
filter_all_quizzes = filter_all_quizzes.exclude(user_id=user.id)
# Favoriten mit mind. 1 Frage
favorite_quiz_ids = []
favorite_quizzes = QivipQuiz.objects.none()
if user:
favorite_quiz_ids = QivipQuizFavorite.objects.filter(user=user, favorite=True).values_list('quiz', flat=True)
favorite_quizzes = QivipQuiz.objects.annotate(max_amount_questions=Count('questions')).filter(
id__in=favorite_quiz_ids,
max_amount_questions__gte=1
)
# Suche anwenden, falls vorhanden
filter_my_quizzes = apply_quiz_filters(filter_my_quizzes, form)
filter_all_quizzes = apply_quiz_filters(filter_all_quizzes, form)
favorite_quizzes = apply_quiz_filters(favorite_quizzes, form)
if request.user.is_authenticated:
filter_all_quizzes = apply_quiz_filters(filter_all_quizzes, form) | apply_quiz_filters(filter_my_quizzes, form)
# Treffer zählen
total_my = filter_my_quizzes.count()
total_all = filter_all_quizzes.count()
total_favorite = favorite_quizzes.count()
total_all_found=total_my + total_all + total_favorite
# Paginierung
paginator_my = Paginator(filter_my_quizzes.order_by('-creation_date'), 8)
paginator_all = Paginator(filter_all_quizzes.order_by('-published_at'), 8)
paginator_fav = Paginator(favorite_quizzes.order_by('-published_at'), 8)
my_page = request.GET.get('my_page', 1)
all_page = request.GET.get('all_page', 1)
fav_page = request.GET.get('favorite_page', 1)
try:
filter_other_quizzes=filter_other_quizzes.exclude(user_id=request.user)
my_quizzes = paginator_my.page(my_page)
except:
filter_other_quizzes=filter_other_quizzes
my_quizzes = paginator_my.page(1)
try:
all_quizzes = paginator_all.page(all_page)
except:
all_quizzes = paginator_all.page(1)
try:
favorite_quizzes = paginator_fav.page(fav_page)
except:
favorite_quizzes = paginator_fav.page(1)
context = {
'form': form,
'my_quizzes': my_quizzes,
'all_quizzes': all_quizzes,
'favorite_quizzes': favorite_quizzes,
'total_my': total_my,
'total_all': total_all,
'total_favorite': total_favorite,
'total_all_found':total_all_found,
'show_search': True,
'favorite_quiz_ids': list(favorite_quiz_ids),
'url_my': '/library/my/',
'url_all': '/library/',
'url_favorite': '/library/favorites/',
}
query_params = request.GET.copy()
querydict_my = query_params.copy()
querydict_all = query_params.copy()
querydict_fav = query_params.copy()
# Entferne die jeweiligen Seiten-Parameter, damit wir sie neu setzen können
querydict_my.pop('my_page', None)
querydict_all.pop('all_page', None)
querydict_fav.pop('favorite_page', None)
# Füge die "bereinigten" Querystrings dem context hinzu
context.update({
'querystring_my': querydict_my.urlencode(),
'querystring_all': querydict_all.urlencode(),
'querystring_favorite': querydict_fav.urlencode(),
})
if connect=="all":
context.update({"link":"library:overview_quiz"})
return render(request, 'library/overview_quiz.html', context)
elif connect=="my":
context.update({"link":"library:overview_quiz_my"})
return render(request, 'library/overview_quiz_my.html', context)
elif connect=="favorite":
context.update({"link":"library:overview_quiz_favorites"})
return render(request, 'library/overview_quiz_favorite.html', context)
def overview_quiz_my(request):
return overview_quiz(request, connect="my")
def overview_quiz_favorites(request):
return overview_quiz(request, connect="favorite")
"""
def overview_quiz(request):
# Filter
form = QuizFilterForm(request.GET)
try:
favorite_quizzes_data = QivipQuizFavorite.objects.filter(user=request.user, favorite=True).order_by("-favorite_date")
favorite_quiz_ids = favorite_quizzes_data.values_list('quiz', flat=True)
favorite_quizzes = QivipQuiz.objects.filter(id__in=favorite_quiz_ids)
except:
favorite_quizzes_data = QivipQuizFavorite.objects.none()
favorite_quizzes = QivipQuizFavorite.objects.none()
favorite_quiz_ids = favorite_quizzes.none()
favorite_quizzes = QivipQuiz.objects.none()
# Initialize querysets
if request.user.is_authenticated:
filter_my_quizzes = QivipQuiz.objects.filter(user_id=request.user).annotate(max_amount_questions=Count('questions'))
favorite_quizzes = favorite_quizzes.filter(id__in=favorite_quiz_ids).annotate(max_amount_questions=Count('questions'))
filter_all_quizzes = QivipQuiz.objects.exclude(user_id=request.user)
else:
favorite_quizzes = QivipQuiz.objects.none()
filter_my_quizzes = QivipQuiz.objects.none()
filter_all_quizzes = QivipQuiz.objects.all()
# Apply common filters to all quizzes
filter_all_quizzes = filter_all_quizzes.annotate(max_amount_questions=Count('questions'))
filter_all_quizzes = filter_all_quizzes.filter(max_amount_questions__gte=1, status='öffentlich')
if form.is_valid():
search = form.cleaned_data.get('search')
username= form.cleaned_data.get('user')
max_amount_questions= form.cleaned_data.get('max_amount_questions')
min_amount_questions= form.cleaned_data.get('min_amount_questions')
filters = {}
if username:
try:
user = User.objects.get(username=username)
filters['user_id'] = user.id
except User.DoesNotExist:
favorite_quizzes = QivipQuiz.objects.none()
filter_my_quizzes = QivipQuiz.objects.none()
filter_all_quizzes=QivipQuiz.objects.none()
if search:
filters['name__icontains'] = search
if min_amount_questions:
filters['max_amount_questions__gte'] = min_amount_questions
if max_amount_questions:
filters['max_amount_questions__lte'] = max_amount_questions
try:
filter_all_quizzes = filter_all_quizzes.filter(**filters)
except:
filter_all_quizzes=QivipQuiz.objects.none()
try:
filter_my_quizzes = filter_my_quizzes.filter(**filters)
except:
filter_my_quizzes=QivipQuiz.objects.none()
try:
favorite_quizzes = favorite_quizzes.filter(**filters)
except:
favorite_quizzes=QivipQuiz.objects.none()
@@ -73,36 +273,108 @@ def overview_quiz(request):
except:
my_quizzes = my_quizzes_paginator.page(1)
# Pagination for other quizzes
other_quizzes_paginator = Paginator(filter_other_quizzes.order_by('-creation_date'), 8) # 8 quizzes per page
# Pagination for all quizzes
all_quizzes_paginator = Paginator(filter_all_quizzes.order_by('-creation_date'), 8) # 8 quizzes per page
try:
other_quizzes = other_quizzes_paginator.page(request.GET.get('other_page', 1))
all_quizzes = all_quizzes_paginator.page(request.GET.get('all_page', 1))
except:
other_quizzes = other_quizzes_paginator.page(1)
all_quizzes = all_quizzes_paginator.page(1)
try:
favorite_quizzes = favorite_quizzes.filter(
favorites__user=request.user, # Filtere nach dem User
favorites__favorite=True # Filtere nach favorisierten Quizzes
).order_by('-favorites__favorite_date')
except:
pass
# Pagination for favorite_quizzes
favorite_quizzes_paginator = Paginator(favorite_quizzes, 8) # 9 quizzes per page
try:
favorite_quizzes = favorite_quizzes_paginator.page(request.GET.get('favorite_page', 1))
except:
favorite_quizzes = favorite_quizzes_paginator.page(1)
context = {
'show_search': True,
'favorite_quiz_ids': favorite_quiz_ids,
'favorite_quizzes':favorite_quizzes,
'my_quizzes': my_quizzes,
'other_quizzes': other_quizzes,
'all_quizzes': all_quizzes,
'form': form,
}
return render(request, 'library/overview_quiz.html', context)
"""
@login_required
def favorite_quiz(request, pk):
quiz= get_object_or_404(QivipQuiz, pk=pk)
favorite = QivipQuizFavorite.objects.filter(user=request.user, quiz=quiz).first()
if favorite:
favorite.favorite=not favorite.favorite
if favorite.favorite==True:
favorite.favorite_date = timezone.now()
favorite.save()
else:
favorite.favorite_date = None
favorite.delete()
statement="Das Quiz '"+ favorite.quiz.name+ "' wurde erfolgreich von den Favoriten entfernt!"
messages.error(request, statement)
else:
favorite=QivipQuizFavorite.objects.create(user=request.user, quiz=quiz, favorite=True, favorite_date=timezone.now())
statement="Das Quiz '"+ favorite.quiz.name+ "' wurde erfolgreich zu den Favoriten hinzugefügt!"
messages.success(request,statement )
return redirect(request.META.get('HTTP_REFERER', 'library:overview_quiz'))
# Neues Quiz erstellen
@login_required
def new_quiz(request):
if request.method == 'POST':
form = QuizForm(request.POST, request.FILES)
if form.is_valid():
quiz = form.save(commit=False)
status = form.cleaned_data.get('status')
if status == 'öffentlich' and not quiz.published_at:
quiz.published_at = timezone.now()
try:
quiz_image=request.FILES['quiz_image']
imagequiz=QuizImage.objects.create(image=quiz_image)
quiz.image=imagequiz
except:
pass
quiz.user_id = request.user
#quiz.creator = request.user
if form.has_changed():
quiz.save()
form.save_m2m() # Speichert die Many-to-Many Beziehungen (Tags)
#form.save_m2m() # Speichert die Many-to-Many Beziehungen (Tags)
return redirect('library:detail_quiz', pk=quiz.pk)
else:
@@ -115,9 +387,39 @@ def edit_quiz(request, pk):
quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
if request.method == 'POST':
form = QuizForm(request.POST, instance=quiz, files=request.FILES)
if form.is_valid():
if 'reset_image' in request.POST:
quiz.image = None
try:
quiz_image=request.FILES['quiz_image']
imagequiz=QuizImage.objects.create(image=quiz_image)
quiz.image=imagequiz
except:
pass
quiz = form.save(commit=False)
# Beispiel: Status aus dem Formular holen
status = form.cleaned_data.get('status')
if status == 'öffentlich' and not quiz.published_at:
quiz.published_at = timezone.now()
if form.has_changed():
form.save()
for img in QuizImage.objects.all():
try:
img.delete()
img.image.delete(save=False)
except:
pass
return redirect('library:detail_quiz', pk=pk)
#return modified(request, pk)
@@ -131,12 +433,24 @@ def delete_quiz(request, pk):
quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
if request.method == 'POST':
quiz.delete()
for img in QuizImage.objects.all():
try:
img.delete()
img.image.delete(save=False)
except:
pass
for img in QuestionImage.objects.all():
try:
img.delete()
img.image.delete(save=False)
except:
pass
return redirect('library:overview_quiz')
return render(request, 'library/delete_confirmation.html', {'object': quiz})
# Quiz anzeigen
def detail_quiz(request, pk):
#quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
quiz = get_object_or_404(QivipQuiz, pk=pk)
show_answers = request.GET.get('show_answers', 'false').lower() == 'true'
questions = QivipQuestion.objects.filter(quiz_id=quiz)
@@ -150,40 +464,19 @@ def detail_quiz(request, pk):
'questions': questions,
'detail':show_answers,
}
if quiz.status!="privat" or quiz.user_id==request.user:
return render(request, 'library/detail_quiz.html', context)
"""
def modified(request, pk):
original_quiz = get_object_or_404(QivipQuiz, pk=pk)
words=original_quiz.modified_description.split()
word_exist=0
print(original_quiz.creator.username)
print(request.user.username)
for word in words:
if word==request.user.username or word+","==request.user.username:
word_exist=1
else:
return redirect('library:overview_quiz')
if word_exist==0 and len(words)!=0 and request.user.username!=original_quiz.creator.username:
original_quiz.modified_description +=", "+ request.user.username
elif word_exist==0 and request.user.username!=original_quiz.creator.username:
original_quiz.modified_description +=" "+ request.user.username
original_quiz.save()
return redirect('library:detail_quiz', pk=pk)
"""
@login_required
def copy_quiz(request, pk):
original_quiz = get_object_or_404(QivipQuiz, pk=pk)
# Quiz kopieren (ohne ID, damit ein neues Objekt erstellt wird)
# Quiz kopieren (ohne ID, damit ein neues Objekt erstellt wird)
@@ -192,9 +485,15 @@ def copy_quiz(request, pk):
new_quiz.pk = None # Setzt die ID auf None, damit Django es als neues Objekt erkennt
new_quiz.uuid = uuid.uuid4() # Neue UUID generieren
new_quiz.user_id = request.user # Neuer Besitzer ist der aktuelle User
new_quiz.name += " - Kopie" # Optional: Name anpassen
new_quiz.name += " - Kopie"
new_quiz.base_quiz_id = pk
#new_quiz.creator = original_quiz.creator or request.user
new_quiz.rating_count = 0
new_quiz.average_rating = 3.0
if original_quiz.status=="öffentlich":
new_quiz.published_at = timezone.now()
else:
new_quiz.published_at = None
new_quiz.save() # Speichern als neues Objekt
# Optional: Beschreibung anpassen
# Alle zugehörigen Fragen kopieren
@@ -204,6 +503,7 @@ def copy_quiz(request, pk):
question.pk = None # Setze pk auf None, damit es als neues Objekt gespeichert wird
question.quiz_id = original_quiz # Verknüpfe mit dem neuen Quiz
question.data = question.data # `data`-Feld wird explizit übernommen # Verknüpfe mit dem neuen Quiz
question.save()
messages.success(request, "Quiz wurde erfolgreich kopiert!")
@@ -238,7 +538,7 @@ def new_question(request):
messages.error(request, 'Quiz nicht gefunden oder keine Berechtigung.')
return redirect('library:overview_quiz')
if question_type not in ['multiple_choice', 'true_false']:
if question_type not in ['multiple_choice', 'true_false','input']:
base_url = reverse('library:new_question')
return redirect(f'{base_url}?type=multiple_choice&quiz_id={quiz_id}')
@@ -256,6 +556,17 @@ def new_question(request):
{'value': 'Falsch', 'is_correct': not correct_answer}
]
}
elif question_type == 'input':
correct_answer = request.POST.get('correct_answer')
json_data = {
'type': question_type,
'question': question_text,
'options': [
{'value': correct_answer, 'is_correct': True}
]
}
elif question_type == 'multiple_choice':
options = []
i = 1
@@ -283,6 +594,20 @@ def new_question(request):
question = QivipQuestion()
question.data = json.dumps(json_data)
question.quiz_id = quiz
try:
question_image=request.FILES['question_image']
imagequestion=QuestionImage.objects.create(image=question_image)
question.image=imagequestion
except:
pass
question.time_per_question = request.POST.get('time_per_question', '30')
try:
question.credits=request.POST.get('credits',"")
except:
question.credits=""
question.save()
#return modified(request, pk=quiz.pk)
return redirect('library:detail_quiz', pk=quiz.pk)
@@ -297,6 +622,19 @@ def new_question(request):
{'value': 'Falsch', 'is_correct': False}
]
}
standard_time_per_question = 30
elif question_type == 'input':
question_data = {
'type': question_type,
'question': '',
'options': [
]
}
standard_time_per_question = 30
elif question_type == 'multiple_choice':
question_data = {
'type': question_type,
@@ -306,12 +644,14 @@ def new_question(request):
{'value': '', 'is_correct': False}
]
}
standard_time_per_question = 30
template_name = f'library/question/question_{question_type}.html'
context = {
'question': question_data,
'quiz_id': quiz_id,
'quiz': quiz
'quiz': quiz,
'standard_time_per_question': standard_time_per_question,
}
return render(request, template_name, context)
@@ -320,6 +660,8 @@ def new_question(request):
@login_required
def edit_question(request, pk):
question = get_object_or_404(QivipQuestion, pk=pk, quiz_id__user_id=request.user)
if 'reset_ques_image' in request.POST:
question.image = None
if question.quiz_id.user_id != request.user:
return redirect('library:question_list')
@@ -327,6 +669,7 @@ def edit_question(request, pk):
try:
question_data = json.loads(question.data) if question.data else {}
question_type = question_data.get('type', 'multiple_choice')
question_image = question.image
# For true/false questions, get the correct answer from the options
if question_type == 'true_false':
@@ -371,6 +714,12 @@ def edit_question(request, pk):
if request.method == 'POST':
question_text = request.POST.get('question', '')
try:
question_image=request.FILES['question_image']
imagequestion=QuestionImage.objects.create(image=question_image)
question.image=imagequestion
except Exception:
pass
# Handle different question types
if question_type == 'true_false':
@@ -383,6 +732,17 @@ def edit_question(request, pk):
{'value': 'Falsch', 'is_correct': not correct_answer}
]
}
elif question_type == 'input':
correct_answer = request.POST.get('correct_answer')
json_data = {
'type': question_type,
'question': question_text,
'options': [
{'value': correct_answer,'is_correct':True}
]
}
elif question_type == 'multiple_choice':
options = []
i = 1
@@ -412,16 +772,31 @@ def edit_question(request, pk):
}
question.data = json.dumps(json_data)
question.time_per_question = request.POST.get('time_per_question', '30')
try:
question.credits=request.POST.get('credits',"")
except:
question.credits=""
question.save()
for img in QuestionImage.objects.all():
try:
img.delete()
img.image.delete(save=False)
except:
pass
#return modified(request, question.quiz_id.pk)
return redirect('library:detail_quiz', pk=question.quiz_id.pk)
else:
template_name = f'library/question/question_{question_type}.html'
context = {
'credits':question.credits,
'question': {'data': question_data}, # Wrap in data structure expected by template
'quiz_id': question.quiz_id.pk,
'quiz': question.quiz_id
'quiz': question.quiz_id,
'time_per_question': question.time_per_question,
'image': question_image,
}
return render(request, template_name, context)
@@ -438,5 +813,11 @@ def delete_question(request, pk):
if request.method == 'POST':
question.delete()
for img in QuestionImage.objects.all():
try:
img.delete()
img.image.delete(save=False)
except:
pass
return redirect('library:detail_quiz', pk=question.quiz_id.pk)
return render(request, 'library/delete_confirmation.html', {'object': question})

913
django/package-lock.json generated
View File

@@ -9,26 +9,907 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"swiper": "^11.2.6"
"@tailwindcss/cli": "^4.1.5",
"tailwindcss": "^4.1.5"
}
},
"node_modules/swiper": {
"version": "11.2.6",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-11.2.6.tgz",
"integrity": "sha512-8aXpYKtjy3DjcbzZfz+/OX/GhcU5h+looA6PbAzHMZT6ESSycSp9nAjPCenczgJyslV+rUGse64LMGpWE3PX9Q==",
"funding": [
{
"type": "patreon",
"url": "https://www.patreon.com/swiperjs"
"node_modules/@parcel/watcher": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
"hasInstallScript": true,
"dependencies": {
"detect-libc": "^1.0.3",
"is-glob": "^4.0.3",
"micromatch": "^4.0.5",
"node-addon-api": "^7.0.0"
},
{
"type": "open_collective",
"url": "http://opencollective.com/swiper"
}
],
"license": "MIT",
"engines": {
"node": ">= 4.7.0"
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.1",
"@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.1",
"@parcel/watcher-freebsd-x64": "2.5.1",
"@parcel/watcher-linux-arm-glibc": "2.5.1",
"@parcel/watcher-linux-arm-musl": "2.5.1",
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
"@parcel/watcher-linux-arm64-musl": "2.5.1",
"@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-linux-x64-musl": "2.5.1",
"@parcel/watcher-win32-arm64": "2.5.1",
"@parcel/watcher-win32-ia32": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.1"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/cli": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.5.tgz",
"integrity": "sha512-Kr567rDwDjY1VUnfqh5/+DCpRf4B8lPs5O9flP4kri7n4AM2aubrIxGSh5GN8s+awUKw/U4+6kNlEnZbBNfUeg==",
"dependencies": {
"@parcel/watcher": "^2.5.1",
"@tailwindcss/node": "4.1.5",
"@tailwindcss/oxide": "4.1.5",
"enhanced-resolve": "^5.18.1",
"mri": "^1.2.0",
"picocolors": "^1.1.1",
"tailwindcss": "4.1.5"
},
"bin": {
"tailwindcss": "dist/index.mjs"
}
},
"node_modules/@tailwindcss/node": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.5.tgz",
"integrity": "sha512-CBhSWo0vLnWhXIvpD0qsPephiaUYfHUX3U9anwDaHZAeuGpTiB3XmsxPAN6qX7bFhipyGBqOa1QYQVVhkOUGxg==",
"dependencies": {
"enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2",
"lightningcss": "1.29.2",
"tailwindcss": "4.1.5"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.5.tgz",
"integrity": "sha512-1n4br1znquEvyW/QuqMKQZlBen+jxAbvyduU87RS8R3tUSvByAkcaMTkJepNIrTlYhD+U25K4iiCIxE6BGdRYA==",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.5",
"@tailwindcss/oxide-darwin-arm64": "4.1.5",
"@tailwindcss/oxide-darwin-x64": "4.1.5",
"@tailwindcss/oxide-freebsd-x64": "4.1.5",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.5",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.5",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.5",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.5",
"@tailwindcss/oxide-linux-x64-musl": "4.1.5",
"@tailwindcss/oxide-wasm32-wasi": "4.1.5",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.5",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.5"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.5.tgz",
"integrity": "sha512-LVvM0GirXHED02j7hSECm8l9GGJ1RfgpWCW+DRn5TvSaxVsv28gRtoL4aWKGnXqwvI3zu1GABeDNDVZeDPOQrw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.5.tgz",
"integrity": "sha512-//TfCA3pNrgnw4rRJOqavW7XUk8gsg9ddi8cwcsWXp99tzdBAZW0WXrD8wDyNbqjW316Pk2hiN/NJx/KWHl8oA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.5.tgz",
"integrity": "sha512-XQorp3Q6/WzRd9OalgHgaqgEbjP3qjHrlSUb5k1EuS1Z9NE9+BbzSORraO+ecW432cbCN7RVGGL/lSnHxcd+7Q==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.5.tgz",
"integrity": "sha512-bPrLWbxo8gAo97ZmrCbOdtlz/Dkuy8NK97aFbVpkJ2nJ2Jo/rsCbu0TlGx8joCuA3q6vMWTSn01JY46iwG+clg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.5.tgz",
"integrity": "sha512-1gtQJY9JzMAhgAfvd/ZaVOjh/Ju/nCoAsvOVJenWZfs05wb8zq+GOTnZALWGqKIYEtyNpCzvMk+ocGpxwdvaVg==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.5.tgz",
"integrity": "sha512-dtlaHU2v7MtdxBXoqhxwsWjav7oim7Whc6S9wq/i/uUMTWAzq/gijq1InSgn2yTnh43kR+SFvcSyEF0GCNu1PQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.5.tgz",
"integrity": "sha512-fg0F6nAeYcJ3CriqDT1iVrqALMwD37+sLzXs8Rjy8Z1ZHshJoYceodfyUwGJEsQoTyWbliFNRs2wMQNXtT7MVA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.5.tgz",
"integrity": "sha512-SO+F2YEIAHa1AITwc8oPwMOWhgorPzzcbhWEb+4oLi953h45FklDmM8dPSZ7hNHpIk9p/SCZKUYn35t5fjGtHA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.5.tgz",
"integrity": "sha512-6UbBBplywkk/R+PqqioskUeXfKcBht3KU7juTi1UszJLx0KPXUo10v2Ok04iBJIaDPkIFkUOVboXms5Yxvaz+g==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.5.tgz",
"integrity": "sha512-hwALf2K9FHuiXTPqmo1KeOb83fTRNbe9r/Ixv9ZNQ/R24yw8Ge1HOWDDgTdtzntIaIUJG5dfXCf4g9AD4RiyhQ==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
"@emnapi/runtime",
"@tybys/wasm-util",
"@emnapi/wasi-threads",
"tslib"
],
"cpu": [
"wasm32"
],
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@emnapi/wasi-threads": "^1.0.2",
"@napi-rs/wasm-runtime": "^0.2.9",
"@tybys/wasm-util": "^0.9.0",
"tslib": "^2.8.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.5.tgz",
"integrity": "sha512-oDKncffWzaovJbkuR7/OTNFRJQVdiw/n8HnzaCItrNQUeQgjy7oUiYpsm9HUBgpmvmDpSSbGaCa2Evzvk3eFmA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.5.tgz",
"integrity": "sha512-WiR4dtyrFdbb+ov0LK+7XsFOsG+0xs0PKZKkt41KDn9jYpO7baE3bXiudPVkTqUEwNfiglCygQHl2jklvSBi7Q==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/jiti": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/lightningcss": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
"integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==",
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-darwin-arm64": "1.29.2",
"lightningcss-darwin-x64": "1.29.2",
"lightningcss-freebsd-x64": "1.29.2",
"lightningcss-linux-arm-gnueabihf": "1.29.2",
"lightningcss-linux-arm64-gnu": "1.29.2",
"lightningcss-linux-arm64-musl": "1.29.2",
"lightningcss-linux-x64-gnu": "1.29.2",
"lightningcss-linux-x64-musl": "1.29.2",
"lightningcss-win32-arm64-msvc": "1.29.2",
"lightningcss-win32-x64-msvc": "1.29.2"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz",
"integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz",
"integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz",
"integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz",
"integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz",
"integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz",
"integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz",
"integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz",
"integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz",
"integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz",
"integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss/node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
"engines": {
"node": ">=8"
}
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
"engines": {
"node": ">=4"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/tailwindcss": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.5.tgz",
"integrity": "sha512-nYtSPfWGDiWgCkwQG/m+aX83XCwf62sBgg3bIlNiiOcggnS1x3uVRDAuyelBFL+vJdOPPCGElxv9DjHJjRHiVA=="
},
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
}
}

View File

@@ -10,6 +10,7 @@
"license": "ISC",
"description": "",
"dependencies": {
"swiper": "^11.2.6"
"@tailwindcss/cli": "^4.1.5",
"tailwindcss": "^4.1.5"
}
}

View File

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

View File

@@ -4,80 +4,10 @@ from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.utils import timezone
from django.urls import reverse
from django.db import models
from datetime import timedelta
from play.models import QuizGame, QuizGameParticipant, QuizAnswer
# Dictionary to track inactive check tasks per game
game_check_tasks = {}
from django.conf import settings
class GameConsumer(AsyncWebsocketConsumer):
@classmethod
async def start_inactive_check(cls, join_code, channel_layer):
"""Start the inactive player check for a game if not already running."""
if join_code not in game_check_tasks or game_check_tasks[join_code].done():
async def check_inactive_players():
game_group_name = f'game_{join_code}'
while True:
try:
await asyncio.sleep(30) # Check every 30 seconds
try:
game = await database_sync_to_async(QuizGame.objects.get)(join_code=join_code)
cutoff_time = timezone.now() - timedelta(minutes=1)
# Get active and inactive participants
all_participants = await database_sync_to_async(lambda: list(
game.participants.all().values('participant_id', 'display_name', 'last_heartbeat')
))()
# Konvertiere datetime zu ISO Format String
for p in all_participants:
if p['last_heartbeat']:
p['last_heartbeat'] = p['last_heartbeat'].isoformat()
active_participants = [p for p in all_participants if p['last_heartbeat'] and timezone.datetime.fromisoformat(p['last_heartbeat']) >= cutoff_time]
inactive_participants = [p for p in all_participants if not p['last_heartbeat'] or timezone.datetime.fromisoformat(p['last_heartbeat']) < cutoff_time]
# Remove inactive participants
if inactive_participants:
for p in inactive_participants:
# Benachrichtige andere über den gekickten Spieler
await channel_layer.group_send(
game_group_name,
{
'type': 'player_left',
'player_name': p['display_name'],
'was_kicked': True
}
)
# Lösche den inaktiven Teilnehmer
await database_sync_to_async(QuizGameParticipant.objects.filter(
participant_id=p['participant_id']
).delete)()
# Broadcast update to all clients
if active_participants or inactive_participants:
await channel_layer.group_send(
game_group_name,
{
'type': 'participant_list_update',
'participants': active_participants
}
)
except QuizGame.DoesNotExist:
break # Stop checking if game no longer exists
except Exception as e:
print(f'Error checking inactive players in game: {e}')
await asyncio.sleep(5) # Wait before retry
except asyncio.CancelledError:
break
except Exception as e:
print(f'Error in game inactive check loop: {e}')
await asyncio.sleep(5)
game_check_tasks[join_code] = asyncio.create_task(check_inactive_players())
async def connect(self):
self.join_code = self.scope['url_route']['kwargs']['join_code']
@@ -91,9 +21,6 @@ class GameConsumer(AsyncWebsocketConsumer):
await self.accept()
# Ensure inactive check is running for this game
await self.start_inactive_check(self.join_code, self.channel_layer)
async def disconnect(self, close_code):
# Leave game group
await self.channel_layer.group_discard(
@@ -146,21 +73,6 @@ class GameConsumer(AsyncWebsocketConsumer):
pass
return
if message_type == 'heartbeat':
participant_id = text_data_json.get('participant_id')
if participant_id:
await self.update_participant_heartbeat(participant_id)
# Update participants list after heartbeat
participants = await self.get_participants()
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'participant_list_update',
'participants': participants
}
)
return
if message_type == 'submit_rating':
participant_id = text_data_json['participant_id']
rating = text_data_json['rating']
@@ -172,7 +84,10 @@ class GameConsumer(AsyncWebsocketConsumer):
elif message_type == 'submit_answer':
participant_id = text_data_json['participant_id']
try:
answer = text_data_json['answer']
except:
answer=-1
time_remaining = text_data_json.get('time_remaining', 0)
# Save answer and update participant score
@@ -250,17 +165,6 @@ class GameConsumer(AsyncWebsocketConsumer):
'redirect_url': event.get('redirect_url')
}))
async def update_participant_heartbeat(self, participant_id):
"""Update the last heartbeat timestamp for a participant."""
try:
participant = await database_sync_to_async(QuizGameParticipant.objects.get)(
participant_id=participant_id
)
participant.last_heartbeat = timezone.now()
await database_sync_to_async(participant.save)()
except QuizGameParticipant.DoesNotExist:
pass
@database_sync_to_async
def verify_host(self, host_id):
try:
@@ -281,15 +185,33 @@ class GameConsumer(AsyncWebsocketConsumer):
# Check if answer is correct
is_correct = False
if answer_index >= 0: # -1 means timeout
question_data = json.loads(current_question.data)
print(answer_index)
try:
if answer_index >= 0 : # -1 means timeout
is_correct = question_data['options'][answer_index]['is_correct']
print(is_correct)
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:
answer_index=int(1)
# Calculate score based on correctness and time
score = 0
if is_correct:
base_score = 1000
time_factor = time_remaining / 30000 # 30 seconds max
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
@@ -311,6 +233,7 @@ class GameConsumer(AsyncWebsocketConsumer):
answer_obj.time_remaining = time_remaining
answer_obj.save()
participant.last_score = score
participant.score += score
participant.last_answer_correct = is_correct
participant.save()
@@ -341,6 +264,9 @@ class GameConsumer(AsyncWebsocketConsumer):
@database_sync_to_async
def save_rating(self, participant_id, rating):
from django.db import models
if not settings.QIVIP_CONFIG['ENABLE_RATING_SYSTEM']:
return False
from library.models import QuizRating
try:
game = QuizGame.objects.get(join_code=self.join_code)
@@ -350,16 +276,23 @@ class GameConsumer(AsyncWebsocketConsumer):
)
# Update or create rating
"""
rating_obj, created = QuizRating.objects.get_or_create(
quiz=game.quiz_id,
participant_id=participant_id,
defaults={'rating': rating}
)
"""
rating_obj=QuizRating.objects.create(
quiz=game.quiz_id,
participant_id=participant_id,
rating=rating)
if not created:
rating_obj.rating = rating
rating_obj.save()
return True
except (QuizGame.DoesNotExist, QuizGameParticipant.DoesNotExist):
return False
@@ -369,20 +302,16 @@ class GameConsumer(AsyncWebsocketConsumer):
try:
game = QuizGame.objects.get(join_code=self.join_code)
# Get all active participants (heartbeat within last minute)
from django.utils import timezone
active_participants = QuizGameParticipant.objects.filter(
quiz_game=game,
last_heartbeat__gte=timezone.now() - timezone.timedelta(minutes=1)
quiz_game=game
).count()
# If no active participants and game is finished for more than 5 minutes, delete it
if active_participants == 0 and game.current_state == 'finished':
if game.question_start_time and (timezone.now() - game.question_start_time).total_seconds() > 300:
if active_participants == 0:
game.delete()
except QuizGame.DoesNotExist:
pass
@database_sync_to_async
def get_participants(self):
try:

View File

@@ -3,53 +3,15 @@ import json
import asyncio
from ..models import QuizGame, QuizGameParticipant
from channels.db import database_sync_to_async
import redis
# Dictionary to track inactive check tasks and active connections per game
game_check_tasks = {}
game_connections = {}
class LobbyConsumer(AsyncWebsocketConsumer):
@classmethod
async def start_inactive_check(cls, join_code, channel_layer):
"""Start the inactive player check for a game if not already running."""
if join_code not in game_check_tasks or game_check_tasks[join_code].done():
async def check_inactive_players():
room_group_name = f'lobby_{join_code}'
while True:
try:
await asyncio.sleep(10) # Check every 10 seconds to match frontend heartbeat
# Get participants list and broadcast update
try:
from django.utils import timezone
from datetime import timedelta
game = await database_sync_to_async(QuizGame.objects.get)(join_code=join_code)
cutoff_time = timezone.now() - timedelta(seconds=20) # Consider inactive after 20 seconds (2 missed heartbeats)
# Get current active participants with full info
active_participants = await database_sync_to_async(lambda: list(
game.participants.filter(last_heartbeat__gte=cutoff_time)
.values('participant_id', 'display_name')
))()
# Send update to all clients
await channel_layer.group_send(
room_group_name,
{
'type': 'participants_list_update',
'participants': active_participants
}
)
except QuizGame.DoesNotExist:
break # Stop checking if game no longer exists
except Exception as e:
print(f'Error checking inactive players: {e}')
await asyncio.sleep(5) # Wait before retry
except asyncio.CancelledError:
break
except Exception as e:
print(f'Error in inactive check loop: {e}')
await asyncio.sleep(5)
game_check_tasks[join_code] = asyncio.create_task(check_inactive_players())
r = redis.Redis(host='localhost', port=6380, db=0, decode_responses=True)
async def connect(self):
self.join_code = self.scope['url_route']['kwargs']['join_code']
@@ -63,9 +25,6 @@ class LobbyConsumer(AsyncWebsocketConsumer):
game_connections[self.join_code] = set()
game_connections[self.join_code].add(self.channel_name)
# Start inactive check task if not already running
await self.start_inactive_check(self.join_code, self.channel_layer)
# Join room group
await self.channel_layer.group_add(
self.room_group_name,
@@ -107,11 +66,6 @@ class LobbyConsumer(AsyncWebsocketConsumer):
}))
return
if message_type == 'heartbeat':
participant_id = data.get('participant_id')
if participant_id:
await self.update_participant_heartbeat(participant_id)
# Don't update participants list after heartbeat - let the background task handle it
elif message_type in ['leave_game', 'kick_player']:
participant_id = data.get('participant_id')
if participant_id:
@@ -180,36 +134,32 @@ class LobbyConsumer(AsyncWebsocketConsumer):
}
)
@database_sync_to_async
def update_participant_heartbeat(self, participant_id):
try:
participant = QuizGameParticipant.objects.get(participant_id=participant_id)
from django.utils import timezone
participant.last_heartbeat = timezone.now()
participant.save()
return True
except QuizGameParticipant.DoesNotExist:
return False
async def get_participants_list(self):
try:
from django.utils import timezone
from datetime import timedelta
game = await database_sync_to_async(QuizGame.objects.get)(join_code=self.join_code)
# Only return participants that have been seen in the last minute
cutoff_time = timezone.now() - timedelta(seconds=20) # Consider inactive after 20 seconds (2 missed heartbeats)
participants = await database_sync_to_async(lambda: list(
game.participants.filter(last_heartbeat__gte=cutoff_time)
game.participants
.values('participant_id', 'display_name')
))()
# Send join notification for new participants
# Zustand laden
try:
prev_names = set(p['display_name'] for p in self.last_participants) if hasattr(self, 'last_participants') else set()
r = redis.Redis(host='localhost', port=6380, db=0, decode_responses=True)
# Alte Teilnehmer aus Redis holen
data = r.get(f"last_participants_{self.room_group_name}")
prev_names = set(json.loads(data)) if data else set()
# Aktuelle Teilnehmer aus dem Argument extrahieren
current_names = set(p['display_name'] for p in participants)
# Neue Teilnehmer bestimmen
new_participants = current_names - prev_names
# Benachrichtigung für neue Teilnehmer versenden
for name in new_participants:
await self.channel_layer.group_send(
self.room_group_name,
@@ -218,9 +168,12 @@ class LobbyConsumer(AsyncWebsocketConsumer):
'player_name': name
}
)
except Exception as e:
print(f'Error sending join notifications: {e}')
# Teilnehmerliste in Redis aktualisieren
r.set(f"last_participants_{self.room_group_name}", json.dumps(list(current_names)))
except Exception as e:
print(f"Error in send_join_notifications: {e}")
self.last_participants = participants
return participants
except QuizGame.DoesNotExist:

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-04-21 11:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0010_alter_quizgameparticipant_last_heartbeat'),
]
operations = [
migrations.AddField(
model_name='quizgame',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
]

View File

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

View File

@@ -19,6 +19,7 @@ class QuizGame(models.Model):
current_state = models.CharField(max_length=20, choices=GAME_STATES, default='lobby')
current_question_index = models.IntegerField(default=0)
question_start_time = models.DateTimeField(null=True, blank=True)
updated_at = models.DateTimeField(auto_now=True)
def save(self, *args, **kwargs):
if not self.join_code:
@@ -42,6 +43,7 @@ class QuizGameParticipant(models.Model):
display_name = models.CharField(verbose_name="Anzeigename", max_length=15)
quiz_game = models.ForeignKey(QuizGame, on_delete=models.CASCADE, related_name="participants")
score = models.IntegerField(verbose_name="Punkte", default=0)
last_score = models.IntegerField(verbose_name="Letzte_Punkte", default=0)
avatar = models.CharField(max_length=200, blank=True, default="")
last_heartbeat = models.DateTimeField(auto_now_add=True)
last_answer = models.IntegerField(null=True, blank=True)

View File

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

View File

@@ -1,26 +1,52 @@
from django.utils import timezone
from django.shortcuts import render
from django.shortcuts import render, redirect, get_object_or_404, reverse
from play.models import QuizGame, QuizGameParticipant
from library.models import QivipQuiz, QivipQuestion
from library.models import QivipQuiz
from django.http import HttpResponseRedirect, HttpResponseNotFound
from django.contrib import messages
from django.conf import settings
import json
import qrcode
import base64
from io import BytesIO
from django.shortcuts import render
# Create your views here.
def lobby(request, join_code):
relative_url = reverse("play:create_participant", kwargs={"join_code": join_code})
full_url = request.build_absolute_uri(relative_url)
qr = qrcode.make(full_url)
buffer = BytesIO()
qr.save(buffer, format="PNG")
img_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
quiz = get_object_or_404(QivipQuiz, pk=quiz_game.quiz_id.id)
context = {
'quiz': quiz,
'quiz_game': quiz_game,
"qr_code_base64": img_base64,
}
mode = request.GET.get('mode','default')
request.session['mode'] = mode
if "host_id" in request.COOKIES:
host_id = request.COOKIES['host_id']
if host_id == quiz_game.host_id:
context['host_id'] = host_id
context["qr_code_base64"] = img_base64
return render(request, 'play/lobby.html', context=context)
elif mode=="learn":
return redirect('play:create_participant', join_code=join_code)
if not "participant_id" in request.COOKIES:
return redirect('play:create_participant', join_code=join_code)
@@ -31,16 +57,25 @@ def lobby(request, join_code):
if not participant.quiz_game.join_code == join_code: # Teilnehmer dem richtigen Spiel hinzufügen
participant.quiz_game = quiz_game
participant.score = 0 # Reset score when joining new game
participant.last_score = 0
participant.save()
except QuizGameParticipant.DoesNotExist:
return redirect('play:create_participant', join_code=join_code)
context['participant'] = participant
context["qr_code_base64"] = img_base64
return render(request, 'play/lobby.html', context=context)
def create_participant(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
def select_mode(request,join_code):
return render(request, 'play/select_mode.html', {'join_code': join_code})
def create_participant(request, join_code):
mode = request.GET.get('mode','default')
request.session['mode'] = mode
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
if mode!="learn":
if "participant_id" in request.COOKIES:
# Prüfe ob der Teilnehmer noch existiert
participant_id = request.COOKIES['participant_id']
@@ -49,6 +84,7 @@ def create_participant(request, join_code):
if participant.quiz_game.join_code != join_code:
participant.quiz_game = quiz_game
participant.score = 0 # Reset score when joining new game
participant.last_score = 0
participant.save()
return redirect('play:lobby', join_code=join_code)
except QuizGameParticipant.DoesNotExist:
@@ -69,6 +105,7 @@ def create_participant(request, join_code):
participant.display_name = display_name
participant.quiz_game = quiz_game
participant.score = 0 # Initialize score
participant.last_score = 0
participant.save()
response = HttpResponseRedirect(reverse('play:lobby', kwargs={'join_code': join_code}))
@@ -76,8 +113,11 @@ def create_participant(request, join_code):
return response
return render(request, 'play/initialize_participant.html', {'join_code': join_code})
else:
messages.error(request, "Beitritt nicht möglich.")
return render(request, 'play/join_game.html')
def create_game(request, quiz_id):
try:
quiz = QivipQuiz.objects.get(id=quiz_id)
game = QuizGame()
@@ -85,7 +125,8 @@ def create_game(request, quiz_id):
game.host_id = game.generate_unique_id()
game.save()
response = HttpResponseRedirect(reverse('play:lobby', kwargs={'join_code': game.join_code}))
response = HttpResponseRedirect(reverse('play:select_mode', kwargs={'join_code': game.join_code}))
response.set_cookie('host_id', game.host_id, max_age=3600)
return response
@@ -106,25 +147,7 @@ def join_game(request):
messages.error(request, "Dieses Spiel existiert nicht.")
return render(request, 'play/join_game.html')
def finished(request, join_code):
try:
game = QuizGame.objects.get(join_code=join_code)
participant = None
participant_id = request.session.get('participant_id')
if participant_id:
participant = QuizGameParticipant.objects.filter(
quiz_game=game,
participant_id=participant_id
).first()
context = {
'join_code': join_code,
'is_host': str(game.host_id) == str(request.session.get('host_id')),
'participant_id': participant_id if participant else None
}
return render(request, 'play/game/finished.html', context)
except QuizGame.DoesNotExist:
return redirect('home')
def scores(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
@@ -146,9 +169,18 @@ def scores(request, join_code):
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/score_overview.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),
@@ -156,8 +188,9 @@ def scores(request, join_code):
'question_index': quiz_game.current_question_index,
'total_questions': len(questions)
})
"""
def finished(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
# Verify game state
@@ -189,6 +222,46 @@ def finished(request, join_code):
'is_host': is_host,
'participant_id': participant_id if participant else None
})
"""
def finished(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
if quiz_game.current_state != 'finished':
return redirect('play:lobby', join_code=join_code)
try:
game = QuizGame.objects.get(join_code=join_code)
# Get participants sorted by score
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game).order_by('-score')
# Get participant if exists
participant_id = request.COOKIES.get('participant_id')
participant = None
if participant_id:
participant = QuizGameParticipant.objects.filter(
quiz_game=quiz_game,
participant_id=participant_id
).first()
is_host = False
if 'host_id' in request.COOKIES and request.COOKIES['host_id'] == quiz_game.host_id:
is_host = True
context = {
'join_code': join_code,
'is_host': is_host,
'participants': participants,
'participant_id': participant_id if participant else None,
'rating_enabled': settings.QIVIP_CONFIG['ENABLE_RATING_SYSTEM']
}
return render(request, 'play/game/finished.html', context)
except QuizGame.DoesNotExist:
return redirect('home')
def question(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
@@ -210,10 +283,12 @@ def question(request, join_code):
return render(request, 'play/game/question_host.html', {
'quiz_game': quiz_game,
'question_data': question_data,
'current_question':current_question,
'host_id': quiz_game.host_id,
'start_time': int(quiz_game.question_start_time.timestamp() * 1000),
'question_index': quiz_game.current_question_index,
'total_questions': len(questions)
'total_questions': len(questions),
'question_image': current_question.image
})
# Handle participant view
@@ -223,16 +298,20 @@ def question(request, join_code):
participant_id = request.COOKIES['participant_id']
try:
participant = QuizGameParticipant.objects.get(participant_id=participant_id)
except QuizGameParticipant.DoesNotExist:
return redirect('play:create_participant', join_code=join_code)
request.session['my_participant-participant_id'] = participant.participant_id
return render(request, 'play/game/question_participant.html', {
'quiz_game': quiz_game,
'question_data': question_data,
'current_question':current_question,
'participant': participant,
'start_time': int(quiz_game.question_start_time.timestamp() * 1000),
'question_index': quiz_game.current_question_index,
'total_questions': len(questions)
'total_questions': len(questions),
})
def waiting_room(request, join_code):
@@ -256,3 +335,43 @@ def waiting_room(request, join_code):
'quiz_game': quiz_game,
'participant': participant
})
def selected_mode(request, join_code):
mode = request.GET.get('mode','default')
request.session['mode'] = mode
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
quiz = get_object_or_404(QivipQuiz, pk=quiz_game.quiz_id.id)
QuizGameParticipant.objects.filter(quiz_game=quiz_game).delete()
context = {
'quiz': quiz,
'quiz_game': quiz_game,
}
if "host_id" in request.COOKIES:
host_id = request.COOKIES['host_id']
if host_id == quiz_game.host_id:
context['host_id'] = host_id
if mode=="learn":
participant = QuizGameParticipant()
if request.user.is_authenticated:
participant.display_name = request.user.username
else:
participant.display_name = "SIE"
participant.participant_id=host_id
participant.quiz_game = quiz_game
participant.score = 0 # Initialize score
participant.last_score = 0
participant.save()
quiz_game.current_state = "question"
quiz_game.question_start_time = timezone.now()
quiz_game.save()
return redirect('play:question',join_code)
return redirect('play:lobby', join_code=join_code)

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

35
django/templates/404.html Normal file
View File

@@ -0,0 +1,35 @@
{% extends 'base.html' %}
{% block content %}
<div class="max-w-2xl mx-auto mt-20 p-6 bg-white shadow-xl rounded-2xl text-center">
<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"
class="w-10 h-10 text-red-600">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</svg>
<h1 class=" text-4xl font-extrabold text-red-600">404 Fehler</h1>
</div>
<div class="my-6 flex justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-24 h-24 text-gray-400">
<path stroke-linecap="round" stroke-linejoin="round"
d="M15.182 16.318A4.486 4.486 0 0 0 12.016 15a4.486 4.486 0 0 0-3.198 1.318M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM9.75 9.75c0 .414-.168.75-.375.75S9 10.164 9 9.75 9.168 9 9.375 9s.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Z" />
</svg>
</div>
<h2 class="text-2xl font-semibold text-gray-800 mb-2 ">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>
<div class="flex justify-center">
<a href="{% url 'homepage:home' %}"
class="bg-blue-600 hover:bg-blue-700 transition-colors text-white px-6 py-3 rounded-full text-lg font-semibold shadow-lg">
Zur Startseite
</a>
</div>
</div>
{% endblock %}

View File

@@ -5,15 +5,37 @@
<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="grid place-content-center h-120">
<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>
<form action="{% url 'accounts:logout' %}" method="post">
{% csrf_token %}
<button class="bg-blue-600 text-white p-3 rounded-full font-black" type="submit">Abmelden</button>
<button class="w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200 cursor-pointer" type="submit">Abmelden</button>
</form>
<button class="text-center text-sm w-full mt-4 font-light text-blue-600"><a href="{% url 'accounts:password_change' %}">Passwort ändern</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="flex items-center gap-2 text-xl font-semibold text-blue-800">
<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" />
</svg>
Erfolge
</div>
<ul class="list-disc pl-6 text-base text-gray-700">
<li><b>{{ my_quizzes_number }}</b> Quizze erstellt</li>
<li><b>{{ my_questions_number }}</b> Fragen insgesamt erstellt</li>
</ul>
</div>
</div>
<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>

View File

@@ -24,9 +24,10 @@
<div class="items-center w-full">
<input class=" w-full p-3 rounded-full bg-gray-200 focus:scale-105 transition duration-200 font-black" type="password" name="password" id="password" required placeholder="PASSWORT">
</div>
<button class="w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200" type="submit" class="login-button">Anmelden</button>
<button class="w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200 cursor-pointer" type="submit" class="login-button">Anmelden</button>
</form>
<button class="text-center text-sm w-full mt-2 font-light text-blue-600"><a href="{% url 'accounts:register' %}" class="register_link">Noch kein Konto?</a></button>
<button class="text-center text-sm w-full mt-2 font-light text-blue-600"><a href="{% url 'accounts:password_reset' %}" class="register_link">Passwort vergessen?</a></button>
</div>
</div>
{% endblock %}

View File

@@ -37,7 +37,14 @@
{{ form.password2 }}
</div>
<div class="register">
{{ form.email }}
</div>
<div class="text-center text-gray-600 text-sm dislay-flex align-bottom">
<label class=''>Mit der Erstellung ihres Kontos stimmen Sie der <a class="text-blue-600" href="/privacy">Datenschutzerklärung </a> zu.</label>
</div>
<button class="w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200" type="submit" class="login-button">Registrieren</button>
</form>
<button class="text-center text-sm w-full mt-2 font-light text-blue-600"><a href="{% url 'accounts:login' %}" class="register_link">Bereits ein Konto?</a></button>

View File

@@ -7,11 +7,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{% static 'css/t-style.css' %}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/@tsparticles/confetti@3.0.3/tsparticles.confetti.bundle.min.js"></script>
<title>qivip</title>
</head>
<body>
{% include 'partials/_nav.html' %}
{% block content%}{% endblock %}
{% block faqs%}{% endblock %}
{% include 'partials/_footer.html' %}
<!-- Toast Container -->
@@ -66,7 +68,12 @@
}, duration);
}
};
if (window.location.pathname !== "/library/") {
localStorage.setItem("meine_seite_scroll", 0);}
</script>
{% block extra_js %}{% endblock %}
<style>html {
overflow-y: scroll;
}</style>
</body>
</html>

View File

@@ -1,22 +0,0 @@
{% extends 'components/base.html' %}
{% load static %}
{% block title %}Antwort{% endblock %}
{% block content %}
<link rel="stylesheet" href="{% static 'css/t-style.css' %}">
<!-- TODO Bedingung festlegen: Quiztyp unterscheiden % if <Bedingung> % -->
{% if user.is_authenticated %}
<button class="overflow-x-auto text-[clamp(0.3rem,5vw,1rem)] break-words border-4 border-gray-500 text-2xl font-black m-0.75 bg-red-500 rounded-sm shadow-[0_2px_2px_rgba(0,0,0,0.1)] text-center text-white">1</button>
<button class="overflow-x-auto text-[clamp(0.3rem,5vw,1rem)] break-words border-4 border-gray-500 text-2xl font-black m-0.75 bg-indigo-500 rounded-sm shadow-[0_2px_2px_rgba(0,0,0,0.1)] text-center text-white">2</button>
<button class="overflow-x-auto text-[clamp(0.3rem,5vw,1rem)] break-words border-4 border-gray-500 text-2xl font-black m-0.75 bg-yellow-500 rounded-sm shadow-[0_2px_2px_rgba(0,0,0,0.1)] text-center text-white">3</button>
<button class="overflow-x-auto text-[clamp(0.3rem,5vw,1rem)] break-words border-4 border-gray-500 text-2xl font-black m-0.75 bg-green-500 rounded-sm shadow-[0_2px_2px_rgba(0,0,0,0.1)] text-center text-white">4</button>
<!-- TODO wieder einfügen, wenn Bedingung gestetzt -->
{% else %}
<button class=" overflow-x-auto text-[clamp(0.3rem,5vw,1rem)] break-words border-4 border-gray-500 text-2xl font-black m-0.75 bg-green-500 rounded-sm shadow-[0_2px_2px_rgba(0,0,0,0.1)] text-center text-white">wahr</button>
<button class=" overflow-x-auto text-[clamp(0.3rem,5vw,1rem)] break-words border-4 border-gray-500 text-2xl font-black m-0.75 bg-red-500 rounded-sm shadow-[0_2px_2px_rgba(0,0,0,0.1)] text-center text-white">falsch</button>
<!--TODO wieder einfügen, wenn Bedingung gestetzt plus -->
{% endif %}
{% endblock %}

View File

@@ -1,45 +0,0 @@
{% load static %}
<!doctype html>
<html class="h-full">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body class="dark:bg-gray-900 h-full m-0">
<div style="height: calc(12vh);" class=" m-0 text-white bg-blue-600 rounded-lg h-12 shadow-md font-semibold px-1 w-full h-20 shadow-[0_2px_2px_rgba(0,0,0,0.1)] break-words text-center flex items-center justify-center overflow-x-auto text-[clamp(0.55rem,5vw,1.5rem)]" > Frage: Wie hoch ist der Eiffelturm insgesamt?
</div>
<div class=" dark:bg-gray-900 flex justify-end gap-2 " style="height: calc(12vh);">
<span class="mt-4 text-[clamp(0.6rem,5vw,1rem)] overflow-x-auto ml-0.75 text-white bg-blue-400 mb-6 h-12 rounded-sm p-3 shadow-[0_2px_2px_rgba(0,0,0,0.1)] z-40 items-center">Zeit: 60s</span>
<div class="mt-4 text-[clamp(0.6rem,5vw,1rem)] overflow-x-auto mr-0.75 bg-gray-500 text-white h-12 mb-6 rounded-sm p-3 shadow-[0_2px_2px_rgba(0,0,0,0.1)] z-40 items-center ">Deine Punkte: 60</div>
</div>
<div class="mt-3 flex justify-center -z-1 " style="height: calc(37vh);"> <img class="object-cover " src="{% static 'components/Ausgabe.png' %}" alt="Bild"> </div>
<div class="w-full grid grid-cols-2 p-2 h-full" style="height: calc(38vh);min-height: 50px;">
{% block content %}
{% endblock %}
</div>
<div class="absolute bottom-0 right-0">
<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>
</div>
 </body>
</html>

View File

@@ -1,63 +0,0 @@
{% load static %}
<link rel="stylesheet" href="{% static 'css/t-style.css' %}">
<!doctype html>
<html class="h-full">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body class=" dark:bg-gray-900 w-full justify-center h-full m-0">
<div style="height: calc(10vh);" class=" m-0 text-white bg-blue-600 rounded-lg h-12 shadow-md font-semibold px-1 w-full h-20 shadow-[0_2px_2px_rgba(0,0,0,0.1)] break-words text-center flex items-center justify-center overflow-x-auto text-[clamp(0.75rem,5vw,1.5rem)]" >Ranking
</div>
<div class="m-4 flex justify-end font-bold">Frage 1/10</div>
<div class="text-[clamp(0.75rem,5vw,1.25rem)] font-bold flex items-center justify-around border-blue-600 border-4 space-x-2 mt-2 overflow-x-auto rounded-lg p-4 shadow-[0_2px_2px_rgba(0,0,0,0.1)] w-full">
<div style="background:linear-gradient(120deg, rgb(255, 208, 0),rgb(216, 151, 21))" class="text-white !important px-4 rounded-sm">1. Platz</div>
<div class="text-stone-900 ">6000 Punkte</div>
<div class="">Person XY</div>
</div>
<div class=" text-[clamp(0.75rem,5vw,1.25rem)] font-bold p-4 flex items-center justify-around border-blue-600 border-4 space-x-2 mt-2 overflow-x-auto rounded-lg shadow-[0_2px_2px_rgba(0,0,0,0.1)] w-full">
<div style="background:linear-gradient(120deg, rgb(102, 122, 122),rgb(194, 186, 186))" class="text-white !important px-4 rounded-sm">2. Platz</div>
<div class="text-stone-900 ">5000 Punkte</div>
<div class="">Person XY</div>
</div>
<div class=" text-[clamp(0.75rem,5vw,1.25rem)] font-bold flex items-center justify-around border-blue-600 border-4 space-x-2 mt-2 overflow-x-auto rounded-lg p-4 shadow-[0_2px_2px_rgba(0,0,0,0.1)] w-full">
<div style="background:linear-gradient(120deg, rgb(211, 91, 17),rgb(219, 168, 109))" class="text-white !important px-4 rounded-sm">3. Platz</div>
<div class="text-stone-900 ">600 Punkte</div>
<div class="">Person XY</div>
</div>
<div class=" text-[clamp(0.75rem,5vw,1.25rem)] items-center flex justify-around font-bold border-blue-600 border-4 space-x-2 mt-2 text-[clamp(0.6rem,5vw,1rem)] overflow-x-auto rounded-lg p-4 shadow-[0_2px_2px_rgba(0,0,0,0.1)] w-full">
<div>4. Platz</div>
<div>200 Punkte</div>
<div>Person XY</div>
</div>
<div class="absolute bottom-0 right-0">
<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>
</div>
 </body>
</html>

View File

@@ -4,7 +4,7 @@
<div class="grid place-content-center md:h-screen h-150 w-full -z-50 top-0 p-4">
<div class="text-center">
<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-2xl text-md">Interaktives Lernen neu definiert.</h3>
<h3 class="mt-4 italic font-extralight sm:text-4xl text-md">weil Lernen auch Spaß <br> machen kann!</h3>
</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">
<a class="qp-a-button bg-green-500 text-white" href="{% url 'play:join_game' %}">Teilnehmen</a>
@@ -13,3 +13,46 @@
</div>
</div>
{% endblock %}
{% block faqs %}
<style>
details[open] summary svg {
transform: rotate(180deg);
}
</style>
{% if faqs %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6">
<div class="flex items-center gap-3">
<h2 class="text-2xl font-bold text-gray-900">FAQs</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div>
</div>
<div class="input-group !max-w-4xl tracking-wide">
<h1 class="font-black text-blue-600 flex justify-between gap-2 lg:text-lg">
FAQs - häufig gestellte Fragen
<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="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" />
</svg></h1>
</div>
{% for faq in faqs %}
<div class="input-group mt-2.5 !max-w-4xl hover:!bg-gray-50">
<details>
<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">
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
</summary>
<p class="text-blue-600">{{ faq.answer }}</p>
</details>
</div>
{% endfor %}
{% endif %}
{% endblock %}

View File

@@ -22,8 +22,9 @@
<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>
</a>
</div>
{% endif %}
</div>
<div class="flex flex-wrap gap-2">
<a href="{% url 'library:copy_quiz' quiz.id %}"
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors duration-200">
@@ -34,6 +35,7 @@
Quiz kopieren
</a>
{% if quiz.questions.count != 0 %}
<a href="{% url 'play:create_game' quiz.id %}"
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors duration-200">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -44,6 +46,7 @@
</svg>
Spielen
</a>
{% endif %}
</div>
</div>
@@ -85,8 +88,18 @@
<div class="bg-white rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md">
<div class="border-b border-gray-200 p-4">
<div class="flex justify-between items-center flex-wrap">
<h2 class="text-xl font-semibold">Fragen</h2>
<h2 class="text-xl font-semibold">Fragen
{% for question in questions %}{% if forloop.last %}({{ forloop.counter }}) {% endif %} {% endfor %}
</h2>
<div class="flex space-x-2 flex-wrap">
{% if quiz.user_id == request.user %}
<a href="{% url 'library:new_question' %}?type=input&quiz_id={{ quiz.id }}"
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
</a>
{% endif %}
{% if quiz.user_id == request.user %}
<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">
@@ -108,24 +121,31 @@
<div class="divide-y divide-gray-200">
{% for question in questions %}
<div class="w-full rounded-xl p-4 hover:bg-gray-50">
<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 %}
<div class="flex justify-between items-start">
<div class="flex-grow">
<p class="font-medium">{{ question.data.question }}</p>
<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{% else %}bg-purple-100 text-purple-800{% endif %}">
{% if question.data.type == 'multiple_choice' %}Multiple Choice{% else %}Wahr/Falsch{% 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 {% else %}bg-purple-100 text-purple-800{% endif %}">
{% if question.data.type == 'multiple_choice' %}Multiple Choice{% elif question.data.type == 'input' %}Eingabe{% else %}Wahr/Falsch{% endif %}
</span>
</div>
<div class="mt-2">
{% if detail %}
{% if question.data.type == 'multiple_choice' %}
<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">
{% 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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
@@ -140,7 +160,29 @@
</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' %}
<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">{{ 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>
{% else %}
<div class="w-full gap-4 md:flex justify-between ">
{% for option in question.data.options %}
{% if option.is_correct %}
<p class="text-sm">
@@ -149,6 +191,10 @@
</p>
{% endif %}
{% endfor %}
{% 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>
{% endif %}
@@ -190,26 +236,45 @@
<div class="flex justify-end">
{% if quiz.base_quiz %}
<p class="text-gray-600 mt-2 ">Bearbeitung:<span class="font-bold"> {{ quiz.user_id}}</span></p>
<p class="text-gray-600 mt-2 ">Bearbeitung:<span class="text-blue-600 font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.user_id }}">{{ quiz.user_id }}</a></span></p>
{% else %}
<p class="text-gray-600 mt-2 ">Autor:<span class="text-blue-600 font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.user_id }}">{{ quiz.user_id }}</a></span></p>
{% endif %}
</div>
<div class="flex justify-end">
{% if quiz.base_quiz %}
<p class="text-gray-600 ">Kopie von: <a href="{% url 'library:detail_quiz' quiz.base_quiz.id %}"><span class="font-bold">{{ quiz.base_quiz}}</span></a></p>
<p class="text-gray-600 ">Kopie von: <a href="{% url 'library:detail_quiz' quiz.base_quiz.id %}"><span class="text-blue-600 font-bold">{{ quiz.base_quiz}}</span></a></p>
{% endif %}
</div>
<div class="flex justify-end">
{% if quiz.base_quiz %}
<p class="text-gray-600 ">Autor:<span class="font-bold"> {{ quiz.base_quiz.user_id}}</span></p>
<p class="text-gray-600 ">Autor:<span class="text-blue-600 font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.base_quiz.user_id }}">{{ quiz.base_quiz.user_id }}</a></span></p>
{% endif %}
</div>
<div class="flex justify-end">
<div class="flex flex-col max-w-full">
Credits:
{% if quiz.credits %}
<p class="text-gray-600 mb-2">Credits:<span class="font-bold"> {{ quiz.credits }}</span></p>
<p class="text-gray-600 mb-2 w-full break-words">
<span class="font-bold break-words">{{ quiz.credits }}</span>
</p>
{% endif %}
</div>
<div class="flex flex-col max-w-full">
<ul class="space-y-2">
{% for question in questions %}
{% if question.credits %}
<li class="text-gray-600 mb-2 w-full break-words font-bold break-words">
{{ question.credits }}
<span class="font-medium text-gray-500 break-words">(Frage: {{ forloop.counter }})</span>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
</div>
{% endblock %}

View File

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

View File

@@ -0,0 +1,145 @@
{% load static %}
{% block extra_css %}
<style>
.custom-outline {
-webkit-text-stroke: 0.2px rgb(0, 0, 0);
}
</style>
{% endblock %}
{% block head %}
<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">
<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" />
</svg>
<span>Filter</span>
</button>
<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">
<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" />
</svg>
Zurücksetzen
</a>
<a href="{% url 'library:new_quiz' %}" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Neues Quiz
</a>
</div>
</div>
<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">
<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">
<div class="space-y-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 %}>
<label class="block text-sm font-medium text-gray-700">minimale Anzahl an Fragen</label>
</div>
<input
type="range"
name="min_amount_questions"
id="id_min_amount_questions"
min="0"
max="100"
step="1"
value="{{ form.min_amount_questions.value|default_if_none:0 }}"
class="w-full accent-blue-600"
oninput="document.getElementById('min_amount_value').textContent = this.value + ' Frage(n)'"
{% if not form.use_min_amount_questions.value %}disabled{% endif %}
>
<span id="min_amount_value" class="text-sm font-semibold text-gray-800">
{{ form.min_amount_questions.value|default_if_none:0 }} Frage(n)
</span>
<br>
<span class="text-xs text-gray-600"> Quizze anderer User werden erst ab min. 1 Frage angezeigt.</span>
</div>
<div class="space-y-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 %}>
<label class="block text-sm font-medium text-gray-700">maximale Anzahl an Fragen</label>
</div>
<input
type="range"
name="max_amount_questions"
id="id_max_amount_questions"
min="1"
max="100"
step="1"
value="{{ form.max_amount_questions.value|default_if_none:100 }}"
class="w-full accent-blue-600"
oninput="document.getElementById('max_amount_value').textContent = this.value+' Frage(n)'"
{% if not form.use_max_amount_questions.value %}disabled{% endif %}
>
<span id="max_amount_value" class="text-sm font-semibold text-gray-800">
{{ form.max_amount_questions.value|default_if_none:100 }} Frage(n)
</span>
</div>
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">veröffentlicht von User</label>
<input type="text" name="user" class="border-2 border-gray-300 rounded-lg p-2 w-full" id="id_user" value="{{ form.user.value|default_if_none:'' }}">
</div>
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">Kategorie</label>
{{ form.categories }}
</div>
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">Suche</label>
{{ form.search }}
</div>
<div class="md:col-span-3 flex justify-end">
<button
type="submit" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200">
<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="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>
</svg>
Filtern
</button>
</div>
</form>
</div>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', () => {
const checkbox = document.getElementById('use_min_amount_questions');
const slider = document.getElementById('id_min_amount_questions');
const checkbox_max = document.getElementById('use_max_amount_questions');
const slider_max = document.getElementById('id_max_amount_questions');
checkbox.addEventListener('change', () => {
slider.disabled = !checkbox.checked;
});
checkbox_max.addEventListener('change', () => {
slider_max.disabled = !checkbox_max.checked;
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,672 @@
{% extends 'base.html' %}
{% load static %}
<!-- Die gesamte Seite wird eigentlich nicht mehr genutzt. WICHTIG: ggf. kann Code aber hiervon noch genutzt werden. -->
{% block extra_css %}
<style>
.custom-outline {
-webkit-text-stroke: 0.2px rgb(0, 0, 0);
}
</style>
{% endblock %}
{% block content %}
<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">
<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" />
</svg>
<span>Filter</span>
</button>
<div class="flex gap-2">
<a href="{% url 'library:overview_quiz' %}" 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">
<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" />
</svg>
Zurücksetzen
</a>
<a href="{% url 'library:new_quiz' %}" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Neues Quiz
</a>
</div>
</div>
<div id="filterPanel" class="container mx-auto px-4">
<div class="bg-white rounded-xl shadow-lg p-6 border border-gray-200">
<form action="{% url 'library:overview_quiz' %}" method="get" class="grid grid-cols-1 md:grid-cols-3 gap-6">
<input type="hidden" name="filter" value="1">
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">minimale Anzahl an Fragen</label>
<input type="number" name="min_amount_questions" class="border-2 border-gray-300 rounded-lg p-2 w-full" min="1" id="id_min_amount_questions" value="{{ form.min_amount_questions.value|default_if_none:'' }}">
</div>
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">maximale Anzahl an Fragen</label>
<input type="number" name="max_amount_questions" class="border-2 border-gray-300 rounded-lg p-2 w-full" min="1" id="id_max_amount_questions" value="{{ form.max_amount_questions.value|default_if_none:'' }}">
</div>
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">veröffentlicht von User</label>
<input type="text" name="user" class="border-2 border-gray-300 rounded-lg p-2 w-full" id="id_user" value="{{ form.user.value|default_if_none:'' }}">
</div>
<div class="md:col-span-3 flex justify-end">
<button type="submit" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200">
<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="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>
</svg>
Filtern
</button>
</div>
</form>
</div>
</div>
{% if user.is_authenticated %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6">
<div class="flex items-center gap-3">
<h2 id="my-quizzes" class="text-2xl font-bold text-gray-900">Meine Quizze</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div>
</div>
<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">
{% for quiz in my_quizzes %}
<article class="relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col">
<!-- Image Section (Top) -->
<div class="h-48 w-full relative flex-shrink-0">
{% if quiz.image %}
<div class="relative w-full h-full inset-0 bg-cover bg-center">
<!-- Hintergrundbild -->
<div class="absolute inset-0 bg-cover bg-center"
style="background-image: url({{ quiz.image.image.url }});">
</div>
</div>
{% else %}
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600 flex items-center justify-center font-black text-white text-4xl px-4 overflow-auto break-words hyphens-auto ">{{ quiz.name }}</div>
{% endif %}
{% if user.is_authenticated %}
{% 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">
<a 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">
<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>
</a>
</div>
{% else %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg">
<a 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">
<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>
</a>
</div>
{% endif %}
{% endif %}
</div>
<!-- Content Section (Bottom) -->
<div class="flex-grow bg-white p-4 flex flex-col gap-3">
<!-- Title -->
<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>
</h2>
<!-- Description -->
<p class="text-sm text-gray-700 break-words mb-3">{{ quiz.description }}</p>
<!-- Quiz Info Grid -->
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<!-- Difficulty -->
<div class="flex items-center gap-2">
<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" />
</svg>
<span class="text-gray-800">{{ quiz.difficulty }}</span>
</div>
<!-- Questions Count -->
<div class="flex items-center gap-2">
<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" />
</svg>
<span class="text-gray-800">{{ quiz.questions.count }} Fragen</span>
</div>
<!-- Status -->
<div class="flex items-center gap-2">
<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" />
</svg>
<span class="text-gray-800">{{ quiz.status }}</span>
</div>
<!-- Rating -->
<div class="flex items-center gap-1">
{% if quiz.rating_count > 0 %}
{% for i in '12345'|make_list %}
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
<span class="text-yellow-500 text-base"></span>
{% else %}
<span class="text-gray-300 text-base"></span>
{% endif %}
{% endfor %}
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
{% else %}
<span class="text-gray-600 text-sm">Noch keine Bewertungen</span>
{% endif %}
</div>
</div>
<!-- Authors -->
{% if quiz.authors.all %}
<div class="flex items-center gap-2 text-sm mt-2">
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-gray-600">
{% for author in quiz.authors.all %}
{{ author.username }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</span>
</div>
{% endif %}
<!-- Action Buttons -->
<div class="flex justify-between items-center gap-2 mt-3 border-t pt-3">
{% if quiz.questions.count != 0 %}
<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">
<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="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Spielen
</a>
{% else %}
Fürs Spielen ist min. 1 Frage nötig!
{% endif %}
<div class="flex gap-1">
<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">
<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="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>
</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">
<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">
<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>
</article>
{% endfor %}
</div>
<!-- Pagination für Meine Quizze -->
{% if my_quizzes.paginator.num_pages > 1 %}
<div class="flex justify-center space-x-2 mt-6 mb-8">
{% if my_quizzes.has_previous %}
<a href="?my_page={{ my_quizzes.previous_page_number }}{% if request.GET.all_page %}&all_page={{ request.GET.all_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</a>
{% endif %}
<span class="px-3 py-1 text-gray-600">Seite {{ my_quizzes.number }} von {{ my_quizzes.paginator.num_pages }}</span>
{% if my_quizzes.has_next %}
<a href="?my_page={{ my_quizzes.next_page_number }}{% if request.GET.all_page %}&all_page={{ request.GET.all_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
{% if user.is_authenticated %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6">
<div class="flex items-center gap-3">
<h2 id="my-quizzes" class="text-2xl font-bold text-gray-900">Meine Favoriten</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div>
</div>
<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">
{% for quiz in favorite_quizzes %}
<article class="relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col">
<!-- Image Section (Top) -->
<div class="h-48 w-full relative flex-shrink-0">
{% if quiz.image %}
<div class="relative w-full h-full inset-0 bg-cover bg-center">
<!-- Hintergrundbild -->
<div class="absolute inset-0 bg-cover bg-center"
style="background-image: url({{ quiz.image.image.url }});">
</div>
</div>
{% else %}
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600 flex items-center justify-center font-black text-white text-4xl px-4 overflow-auto break-words hyphens-auto ">{{ quiz.name }}</div>
{% endif %}
{% if user.is_authenticated %}
{% 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">
<a 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">
<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>
</a>
</div>
{% else %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg">
<a 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">
<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>
</a>
</div>
{% endif %}
{% endif %}
</div>
<!-- Content Section (Bottom) -->
<div class="flex-grow bg-white p-4 flex flex-col gap-3">
<!-- Title -->
<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>
</h2>
<!-- Description -->
<p class="text-sm text-gray-700 break-words mb-3">{{ quiz.description }}</p>
<!-- Quiz Info Grid -->
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<!-- Difficulty -->
<div class="flex items-center gap-2">
<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" />
</svg>
<span class="text-gray-800">{{ quiz.difficulty }}</span>
</div>
<!-- Questions Count -->
<div class="flex items-center gap-2">
<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" />
</svg>
<span class="text-gray-800">{{ quiz.questions.count }} Fragen</span>
</div>
<!-- Status -->
<div class="flex items-center gap-2">
<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" />
</svg>
<span class="text-gray-800">{{ quiz.status }}</span>
</div>
<!-- Rating -->
<div class="flex items-center gap-1">
{% if quiz.rating_count > 0 %}
{% for i in '12345'|make_list %}
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
<span class="text-yellow-500 text-base"></span>
{% else %}
<span class="text-gray-300 text-base"></span>
{% endif %}
{% endfor %}
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
{% else %}
<span class="text-gray-600 text-sm">Noch keine Bewertungen</span>
{% endif %}
</div>
</div>
<!-- Authors -->
{% if quiz.authors.all %}
<div class="flex items-center gap-2 text-sm mt-2">
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-gray-600">
{% for author in quiz.authors.all %}
{{ author.username }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</span>
</div>
{% endif %}
<!-- Action Buttons -->
<div class="flex justify-between items-center gap-2 mt-3 border-t pt-3">
{% if quiz.questions.count != 0 %}
<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">
<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="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Spielen
</a>
{% else %}
Fürs Spielen ist min. 1 Frage nötig!
{% endif %}
<div class="flex gap-1">
<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">
<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="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>
</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">
<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">
<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>
</article>
{% endfor %}
</div>
<!-- Pagination für Meine Quizze -->
{% if favorite_quizzes.paginator.num_pages > 1 %}
<div class="flex justify-center space-x-2 mt-6 mb-8">
{% if favorite_quizzes.has_previous %}
<a href="?my_page={{ favorite.previous_page_number }}{% if request.GET.all_page %}&all_page={{ request.GET.all_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</a>
{% endif %}
<span class="px-3 py-1 text-gray-600">Seite {{ favorite_quizzes.number }} von {{ favorite_quizzes.paginator.num_pages }}</span>
{% if favorite_quizzes.has_next %}
<a href="?favorite_page={{ favorite_quizzes.next_page_number }}{% if request.GET.all_page %}&all_page={{ request.GET.all_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
<!-- Alle -->
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6">
<div class="flex items-center gap-3">
<h2 id="all-quizzes" class="text-2xl font-bold text-gray-900">Alle</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div>
</div>
{% if all_quizzes %}
<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">
{% for quiz in all_quizzes %}
<article class="relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col">
<!-- Image Section (Top) -->
<div class="h-48 w-full relative flex-shrink-0">
{% if quiz.image %}
<div class="relative w-full h-full inset-0 bg-cover bg-center">
<!-- Hintergrundbild -->
<div class="absolute inset-0 bg-cover bg-center"
style="background-image: url({{ quiz.image.image.url }});">
</div>
</div>
{% else %}
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600 flex items-center justify-center font-black text-white text-4xl px-4 overflow-auto break-words hyphens-auto ">{{ quiz.name }}</div>
{% endif %}
{% if user.is_authenticated %}
{% 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">
<a 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">
<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>
</a>
</div>
{% else %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg">
<a 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">
<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>
</a>
</div>
{% endif %}
{% endif %}
</div>
<!-- Content Section (Bottom) -->
<div class="flex-grow bg-white p-4 flex flex-col gap-3">
<!-- Title -->
<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>
</h2>
<!-- Description -->
<p class="text-sm text-gray-700 break-words mb-3">{{ quiz.description }}</p>
<!-- Quiz Info Grid -->
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<!-- Difficulty -->
<div class="flex items-center gap-2">
<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" />
</svg>
<span class="text-gray-800">{{ quiz.difficulty }}</span>
</div>
<!-- Questions Count -->
<div class="flex items-center gap-2">
<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" />
</svg>
<span class="text-gray-800">{{ quiz.questions.count }} Fragen</span>
</div>
<!-- Status -->
<div class="flex items-center gap-2">
<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" />
</svg>
<span class="text-gray-800">{{ quiz.status }}</span>
</div>
<!-- Rating -->
<div class="flex items-center gap-1">
{% if quiz.rating_count > 0 %}
{% for i in '12345'|make_list %}
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
<span class="text-yellow-500 text-base"></span>
{% else %}
<span class="text-gray-300 text-base"></span>
{% endif %}
{% endfor %}
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
{% else %}
<span class="text-gray-600 text-sm">Noch keine Bewertungen</span>
{% endif %}
</div>
</div>
<!-- Authors -->
{% if quiz.authors.all %}
<div class="flex items-center gap-2 text-sm mt-2">
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-gray-600">
{% for author in quiz.authors.all %}
{{ author.username }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</span>
</div>
{% endif %}
<!-- Action Buttons -->
<div class="flex justify-between items-center gap-2 mt-3 border-t pt-3">
{% if quiz.questions.count != 0 %}
<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">
<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="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Spielen
</a>
{% else %}
Fürs Spielen ist min. 1 Frage nötig!
{% endif %}
<div class="flex gap-1">
<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">
<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="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>
</a>
</div>
</div>
</div>
</article>
{% endfor %}
</div>
<!-- Pagination für andere Quiz -->
{% if all_quizzes.paginator.num_pages > 1 %}
<div class="flex justify-center space-x-2 mt-6 mb-8">
{% if all_quizzes.has_previous %}
<a href="?all_page={{ all_quizzes.previous_page_number }}{% if request.GET.my_page %}&my_page={{ request.GET.my_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</a>
{% endif %}
<span class="px-3 py-1 text-gray-600">Seite {{ all_quizzes.number }} von {{ all_quizzes.paginator.num_pages }}</span>
{% if all_quizzes.has_next %}
<a href="?all_page={{ all_quizzes.next_page_number }}{% if request.GET.my_page %}&my_page={{ request.GET.my_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
{% endif %}
</div>
{% endif %}
</div>
{% else %}
<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>
</div>
{% endif %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('filter') === '1') {
document.getElementById('filterPanel').classList.add('show');
}
document.getElementById('filterButton').addEventListener('click', function() {
document.getElementById('filterPanel').classList.toggle('show');
// URL beim Öffnen aktualisieren
const params = new URLSearchParams(window.location.search);
if (document.getElementById('filterPanel').classList.contains('show')) {
params.set('filter', '1');
} else {
params.delete('filter');
}
history.replaceState(null, '', `${location.pathname}?${params}`);
});
});
if (window.location.pathname.includes("/library/")) {
window.addEventListener("beforeunload", function () {
localStorage.setItem("scroll-position", window.scrollY);
});
window.addEventListener("load", function () {
const scrollPos = localStorage.getItem("scroll-position");
if (scrollPos !== null) {
window.scrollTo(0, parseInt(scrollPos));
}
});
}
</script>
{% endblock content %}

View File

@@ -1,22 +1,9 @@
{% extends 'base.html' %}
{% load static %}
{% load humanize %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'swiper/swiper-bundle.min.css' %}">
<style>
.swiper {
width: 100%;
height: 100%;
}
.swiper-slide {
text-align: center;
font-size: 18px;
background: #fff;
display: flex;
justify-content: center;
align-items: center;
}
.custom-outline {
-webkit-text-stroke: 0.2px rgb(0, 0, 0);
@@ -26,76 +13,81 @@
{% block content %}
<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">
<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" />
</svg>
<span>Filter</span>
</button>
<div class="flex gap-2">
<a href="{% url 'library:overview_quiz' %}" 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">
<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" />
</svg>
Zurücksetzen
</a>
<a href="{% url 'library:new_quiz' %}" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Neues Quiz
</a>
</div>
</div>
{% block head %}
{% include 'library/head.html' %}
{% endblock %}
<div id="filterPanel" class="container mx-auto px-4">
<div class="bg-white rounded-xl shadow-lg p-6 border border-gray-200">
<form method="get" class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">Minimale Fragen</label>
{{ form.min_amout_questions }}
</div>
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">Maximale Fragen</label>
{{ form.max_amout_questions }}
</div>
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">Von User</label>
{{ form.user }}
</div>
<div class="md:col-span-3 flex justify-end">
<button type="submit" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200">
<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="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>
Filtern
</button>
</div>
</form>
</div>
</div>
{% if user.is_authenticated %}
<!-- Alle -->
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6">
{% if user.is_authenticated %}
<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">
<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>
<div class="gap-2 mx-2">
Anzahl aller Ergebnisse:
<span class="font-bold"> {{ total_all_found }}</span>
</div>
(verschiedene:
<span class="font-bold ml-1"> {{ total_all }}</span>) </div>
<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>
<a href="{{ url_my }}?{{ request.GET.urlencode }}">Meine Quizze ({{ total_my }})</a>
<a href="{{ url_favorite }}?{{ request.GET.urlencode }}">Meine Favoriten ({{ total_favorite }})</a>
</div>
{% endif %}
<div class="flex items-center gap-3">
<h2 id="my-quizzes" class="text-2xl font-bold text-gray-900">Eigene Quiz</h2>
<h2 id="all-quizzes" class="text-2xl font-bold text-gray-900">Alle</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div>
</div>
{% if all_quizzes %}
<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">
{% for quiz in my_quizzes %}
{% for quiz in all_quizzes %}
<article class="relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col">
<!-- Image Section (Top) -->
<div class="h-48 w-full relative flex-shrink-0">
{% if quiz.image %}
<div class="absolute inset-0 bg-cover bg-center" style="background-image: url({{ quiz.image.url }});"></div>
<div class="relative w-full h-full inset-0 bg-cover bg-center">
<!-- Hintergrundbild -->
<div class="absolute inset-0 bg-cover bg-center"
style="background-image: url({{ quiz.image.image.url }});">
</div>
</div>
{% else %}
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600"></div>
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600 flex items-center justify-center font-black text-white text-3xl px-4 overflow-auto break-words hyphens-auto ">{{ quiz.name }}</div>
{% endif %}
{% if user.is_authenticated %}
{% 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">
<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">
<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>
</a>
</div>
{% else %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg">
<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">
<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>
</a>
</div>
{% endif %}
{% endif %}
</div>
@@ -134,8 +126,11 @@
<span class="text-gray-800">{{ quiz.status }}</span>
</div>
<!-- Rating -->
<div class="flex items-center gap-1">
{% if quiz.rating_count > 0 %}
{% for i in '12345'|make_list %}
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
<span class="text-yellow-500 text-base"></span>
@@ -144,177 +139,40 @@
{% endif %}
{% endfor %}
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
{% else %}
<span class="text-gray-600 text-sm">Noch keine Bewertungen</span>
{% endif %}
</div>
</div>
<!-- Authors -->
{% if quiz.authors.all %}
<!--
{% if quiz.user_id %}
<div class="flex items-center gap-2 text-sm mt-2">
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-gray-600">
{% for author in quiz.authors.all %}
{{ author.username }}{% if not forloop.last %}, {% endif %}
{% endfor %}
{{ quiz.user_id.username }}
</span>
</div>
{% endif %}
{% endif %}
-->
<!-- Action Buttons -->
<div class="flex justify-between items-center gap-2 mt-3 border-t pt-3">
<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">
<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="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Spielen
</a>
<div class="flex gap-1">
<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">
<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="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>
</a>
{% if quiz.user == 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">
<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">
<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>
</article>
{% endfor %}
<!-- Zeit-->
{% if quiz.published_at != None %}
</div>
<!-- Pagination für eigene Quiz -->
{% if my_quizzes.paginator.num_pages > 1 %}
<div class="flex justify-center space-x-2 mt-6 mb-8">
{% if my_quizzes.has_previous %}
<a href="?my_page={{ my_quizzes.previous_page_number }}{% if request.GET.other_page %}&other_page={{ request.GET.other_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</a>
{% endif %}
<span class="px-3 py-1 text-gray-600">Seite {{ my_quizzes.number }} von {{ my_quizzes.paginator.num_pages }}</span>
{% if my_quizzes.has_next %}
<a href="?my_page={{ my_quizzes.next_page_number }}{% if request.GET.other_page %}&other_page={{ request.GET.other_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
{% endif %}
</div>
{% endif %}
<div class="flex items-center gap-2 ">
<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" />
</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>
</div>
{% endif %}
<!-- Quiz von anderen Nutzern -->
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6">
<div class="flex items-center gap-3">
<h2 id="other-quizzes" class="text-2xl font-bold text-gray-900">Quiz von anderen Nutzern</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div>
</div>
{% if other_quizzes %}
<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">
{% for quiz in other_quizzes %}
<article class="relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col">
<!-- Image Section (Top) -->
<div class="h-48 w-full relative flex-shrink-0">
{% if quiz.image %}
<div class="absolute inset-0 bg-cover bg-center" style="background-image: url({{ quiz.image.url }});"></div>
{% else %}
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600"></div>
{% endif %}
</div>
<!-- Content Section (Bottom) -->
<div class="flex-grow bg-white p-4 flex flex-col gap-3">
<!-- Title -->
<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>
</h2>
<!-- Description -->
<p class="text-sm text-gray-700 break-words mb-3">{{ quiz.description }}</p>
<!-- Quiz Info Grid -->
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<!-- Difficulty -->
<div class="flex items-center gap-2">
<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" />
</svg>
<span class="text-gray-800">{{ quiz.difficulty }}</span>
</div>
<!-- Questions Count -->
<div class="flex items-center gap-2">
<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" />
</svg>
<span class="text-gray-800">{{ quiz.questions.count }} Fragen</span>
</div>
<!-- Status -->
<div class="flex items-center gap-2">
<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" />
</svg>
<span class="text-gray-800">{{ quiz.status }}</span>
</div>
<!-- Rating -->
<div class="flex items-center gap-1">
{% for i in '12345'|make_list %}
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
<span class="text-yellow-500 text-base"></span>
{% else %}
<span class="text-gray-300 text-base"></span>
{% endif %}
{% endfor %}
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
</div>
</div>
<!-- Authors -->
{% if quiz.authors.all %}
<div class="flex items-center gap-2 text-sm mt-2">
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-gray-600">
{% for author in quiz.authors.all %}
{{ author.username }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</span>
</div>
{% endif %}
<!-- Action Buttons -->
<div class="flex justify-between items-center gap-2 mt-3 border-t pt-3">
{% if quiz.questions.count != 0 %}
<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">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -323,6 +181,9 @@
</svg>
Spielen
</a>
{% else %}
Fürs Spielen ist min. 1 Frage nötig!
{% endif %}
<div class="flex gap-1">
<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">
@@ -339,10 +200,10 @@
</div>
<!-- Pagination für andere Quiz -->
{% if other_quizzes.paginator.num_pages > 1 %}
{% if all_quizzes.paginator.num_pages > 1 %}
<div class="flex justify-center space-x-2 mt-6 mb-8">
{% if other_quizzes.has_previous %}
<a href="?other_page={{ other_quizzes.previous_page_number }}{% if request.GET.my_page %}&my_page={{ request.GET.my_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
{% if all_quizzes.has_previous %}
<a href="?all_page={{ all_quizzes.previous_page_number }}&{{ querystring_all }}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
@@ -350,10 +211,10 @@
</a>
{% endif %}
<span class="px-3 py-1 text-gray-600">Seite {{ other_quizzes.number }} von {{ other_quizzes.paginator.num_pages }}</span>
<span class="px-3 py-1 text-gray-600">Seite {{ all_quizzes.number }} von {{ all_quizzes.paginator.num_pages }}</span>
{% if other_quizzes.has_next %}
<a href="?other_page={{ other_quizzes.next_page_number }}{% if request.GET.my_page %}&my_page={{ request.GET.my_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
{% if all_quizzes.has_next %}
<a href="?all_page={{ all_quizzes.next_page_number }}&{{ querystring_all }}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
@@ -367,12 +228,54 @@
<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>
</div>
{% endif %}
<script>
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('filterButton').addEventListener('click', function() {
document.getElementById('filterPanel').classList.toggle('show');
{% endif %}<script>
document.addEventListener('DOMContentLoaded', function () {
const filterPanel = document.getElementById('filterPanel');
const filterButton = document.getElementById('filterButton');
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('filter') === '1') {
filterPanel.classList.add('show');
}
filterButton.addEventListener('click', function () {
filterPanel.classList.toggle('show');
const params = new URLSearchParams(window.location.search);
if (filterPanel.classList.contains('show')) {
//params.set('filter', '1');
} else {
params.delete('filter');
}
history.replaceState(null, '', `${location.pathname}?${params}`);
});
});
document.addEventListener("DOMContentLoaded", function () {
// Scroll-Position beim Laden setzen
const scrollPos = localStorage.getItem("scroll-position");
if (scrollPos !== null) {
window.scrollTo(0, parseInt(scrollPos));
localStorage.removeItem("scroll-position");
}
// Nur Favoriten-Links abfangen
const favoriteLinks = document.querySelectorAll('a.favorite-link');
favoriteLinks.forEach(link => {
link.addEventListener('click', function () {
localStorage.setItem("scroll-position", window.scrollY);
});
});
});
</script>
{% endblock content %}

View File

@@ -0,0 +1,297 @@
{% extends 'base.html' %}
{% load static %}
{% load humanize %}
{% block extra_css %}
<style>
.custom-outline {
-webkit-text-stroke: 0.2px rgb(0, 0, 0);
}
</style>
{% endblock %}
{% block content %}
{% block head %}
{% include 'library/head.html' %}
{% endblock %}
{% if user.is_authenticated %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6">
<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">
<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>
<div class="gap-2 mx-2">
Anzahl aller Ergebnisse:
<span class="font-bold"> {{ total_all_found }}</span>
</div>
(verschiedene:
<span class="font-bold ml-1"> {{ total_all }}</span>) </div>
<div class="flex flex-wrap justify-end gap-4 font-bold text-blue-600 whitespace-nowrap text-sm mb-2 md:mb-0">
<a href="{{ url_all }}?{{ request.GET.urlencode }}">Alle ({{ total_all }})</a>
<a href="{{ url_my }}?{{ request.GET.urlencode }}">Meine Quizze ({{ total_my }})</a>
<p class="font-black underline">Meine Favoriten ({{ total_favorite }})</p>
</div>
{% endif %}
<div class="flex items-center gap-3">
<h2 id="my-quizzes" class="text-2xl font-bold text-gray-900">Meine Favoriten</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div>
</div>
{% if request.user.is_authenticated and favorite_quizzes %}
<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">
{% for quiz in favorite_quizzes %}
<article class="relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col">
<!-- Image Section (Top) -->
<div class="h-48 w-full relative flex-shrink-0">
{% if quiz.image %}
<div class="relative w-full h-full inset-0 bg-cover bg-center">
<!-- Hintergrundbild -->
<div class="absolute inset-0 bg-cover bg-center"
style="background-image: url({{ quiz.image.image.url }});">
</div>
</div>
{% else %}
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600 flex items-center justify-center font-black text-white text-3xl px-4 overflow-auto break-words hyphens-auto ">{{ quiz.name }}</div>
{% endif %}
{% if user.is_authenticated %}
{% 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">
<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">
<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>
</a>
</div>
{% else %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg">
<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">
<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>
</a>
</div>
{% endif %}
{% endif %}
</div>
<!-- Content Section (Bottom) -->
<div class="flex-grow bg-white p-4 flex flex-col gap-3">
<!-- Title -->
<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>
</h2>
<!-- Description -->
<p class="text-sm text-gray-700 break-words mb-3">{{ quiz.description }}</p>
<!-- Quiz Info Grid -->
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<!-- Difficulty -->
<div class="flex items-center gap-2">
<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" />
</svg>
<span class="text-gray-800">{{ quiz.difficulty }}</span>
</div>
<!-- Questions Count -->
<div class="flex items-center gap-2">
<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" />
</svg>
<span class="text-gray-800">{{ quiz.questions.count }} Fragen</span>
</div>
<!-- Status -->
<div class="flex items-center gap-2">
<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" />
</svg>
<span class="text-gray-800">{{ quiz.status }}</span>
</div>
<!-- Rating -->
<div class="flex items-center gap-1">
{% if quiz.rating_count > 0 %}
{% for i in '12345'|make_list %}
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
<span class="text-yellow-500 text-base"></span>
{% else %}
<span class="text-gray-300 text-base"></span>
{% endif %}
{% endfor %}
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
{% else %}
<span class="text-gray-600 text-sm">Noch keine Bewertungen</span>
{% endif %}
</div>
</div>
<!-- Authors -->
<!--
{% if quiz.authors.all %}
<div class="flex items-center gap-2 text-sm mt-2">
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-gray-600">
{% for author in quiz.authors.all %}
{{ author.username }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</span>
</div>
{% endif %}-->
<!-- Zeit-->
{% if quiz.published_at != None %}
<div class="flex items-center gap-2 ">
<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" />
</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>
</div>
{% endif %}
<!-- Action Buttons -->
<div class="flex justify-between items-center gap-2 mt-3 border-t pt-3">
{% if quiz.questions.count != 0 %}
<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">
<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="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Spielen
</a>
{% else %}
Fürs Spielen ist min. 1 Frage nötig!
{% endif %}
<div class="flex gap-1">
<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">
<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="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>
</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">
<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">
<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>
</article>
{% endfor %}
</div>
<!-- Pagination für Meine Quizze -->
{% if favorite_quizzes.paginator.num_pages > 1 %}
<div class="flex justify-center space-x-2 mt-6 mb-8">
{% if favorite_quizzes.has_previous %}
<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">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</a>
{% endif %}
<span class="px-3 py-1 text-gray-600">Seite {{ favorite_quizzes.number }} von {{ favorite_quizzes.paginator.num_pages }}</span>
{% if favorite_quizzes.has_next %}
<a href="?favorite_page={{ favorite_quizzes.next_page_number }}&{{ querystring_favorite }}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
{% endif %}
</div>
{% endif %}
</div>
{% else %}
<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>
</div>
{% endif %}<script>
document.addEventListener('DOMContentLoaded', function () {
const filterPanel = document.getElementById('filterPanel');
const filterButton = document.getElementById('filterButton');
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('filter') === '1') {
filterPanel.classList.add('show');
}
filterButton.addEventListener('click', function () {
filterPanel.classList.toggle('show');
const params = new URLSearchParams(window.location.search);
if (filterPanel.classList.contains('show')) {
//params.set('filter', '1');
} else {
params.delete('filter');
}
history.replaceState(null, '', `${location.pathname}?${params}`);
});
});
document.addEventListener("DOMContentLoaded", function () {
// Scroll-Position beim Laden setzen
const scrollPos = localStorage.getItem("scroll-position");
if (scrollPos !== null) {
window.scrollTo(0, parseInt(scrollPos));
localStorage.removeItem("scroll-position");
}
// Nur Favoriten-Links abfangen
const favoriteLinks = document.querySelectorAll('a.favorite-link');
favoriteLinks.forEach(link => {
link.addEventListener('click', function () {
localStorage.setItem("scroll-position", window.scrollY);
});
});
});
</script>
{% endblock content %}

View File

@@ -0,0 +1,294 @@
{% extends 'base.html' %}
{% load static %}
{% load humanize %}
{% block extra_css %}
<style>
.custom-outline {
-webkit-text-stroke: 0.2px rgb(0, 0, 0);
}
</style>
{% endblock %}
{% block content %}
{% block head %}
{% include 'library/head.html' %}
{% endblock %}
{% if user.is_authenticated %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6">
<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">
<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>
<div class="gap-2 mx-2">
Anzahl aller Ergebnisse:
<span class="font-bold"> {{ total_all_found }}</span>
</div>
(verschiedene:
<span class="font-bold ml-1"> {{ total_all }}</span>) </div>
<div class="flex flex-wrap justify-end gap-4 font-bold text-blue-600 whitespace-nowrap text-sm mb-2 md:mb-0">
<a href="{{ url_all }}?{{ request.GET.urlencode }}">Alle ({{ total_all }})</a>
<p class="font-black underline">Meine Quizze ({{ total_my }})</p>
<a href="{{ url_favorite }}?{{ request.GET.urlencode }}">Meine Favoriten ({{ total_favorite }})</a>
</div>
<div class="flex items-center gap-3">
<h2 id="my-quizzes" class="text-2xl font-bold text-gray-900">Meine Quizze</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div>
</div>
{% endif %}
{% if request.user.is_authenticated and my_quizzes %}
<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">
{% for quiz in my_quizzes %}
<article class="relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col">
<!-- Image Section (Top) -->
<div class="h-48 w-full relative flex-shrink-0">
{% if quiz.image %}
<div class="relative w-full h-full inset-0 bg-cover bg-center">
<!-- Hintergrundbild -->
<div class="absolute inset-0 bg-cover bg-center"
style="background-image: url({{ quiz.image.image.url }});">
</div>
</div>
{% else %}
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600 flex items-center justify-center font-black text-white text-3xl px-4 overflow-auto break-words hyphens-auto ">{{ quiz.name }}</div>
{% endif %}
{% if user.is_authenticated %}
{% 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">
<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">
<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>
</a>
</div>
{% else %}
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg">
<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">
<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>
</a>
</div>
{% endif %}
{% endif %}
</div>
<!-- Content Section (Bottom) -->
<div class="flex-grow bg-white p-4 flex flex-col gap-3">
<!-- Title -->
<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>
</h2>
<!-- Description -->
<p class="text-sm text-gray-700 break-words mb-3">{{ quiz.description }}</p>
<!-- Quiz Info Grid -->
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<!-- Difficulty -->
<div class="flex items-center gap-2">
<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" />
</svg>
<span class="text-gray-800">{{ quiz.difficulty }}</span>
</div>
<!-- Questions Count -->
<div class="flex items-center gap-2">
<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" />
</svg>
<span class="text-gray-800">{{ quiz.questions.count }} Fragen</span>
</div>
<!-- Status -->
<div class="flex items-center gap-2">
<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" />
</svg>
<span class="text-gray-800">{{ quiz.status }}</span>
</div>
<!-- Rating -->
<div class="flex items-center gap-1">
{% if quiz.rating_count > 0 %}
{% for i in '12345'|make_list %}
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
<span class="text-yellow-500 text-base"></span>
{% else %}
<span class="text-gray-300 text-base"></span>
{% endif %}
{% endfor %}
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
{% else %}
<span class="text-gray-600 text-sm">Noch keine Bewertungen</span>
{% endif %}
</div>
</div>
<!-- Authors -->
<!--
{% if quiz.authors.all %}
<div class="flex items-center gap-2 text-sm mt-2">
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-gray-600">
{% for author in quiz.authors.all %}
{{ author.username }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</span>
</div>
{% endif %}
-->
<!-- Zeit-->
{% if quiz.published_at != None %}
<div class="flex items-center gap-2 ">
<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" />
</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>
</div>
{% endif %}
<!-- Action Buttons -->
<div class="flex justify-between items-center gap-2 mt-3 border-t pt-3">
{% if quiz.questions.count != 0 %}
<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">
<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="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Spielen
</a>
{% else %}
Fürs Spielen ist min. 1 Frage nötig!
{% endif %}
<div class="flex gap-1">
<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">
<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="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>
</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">
<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">
<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>
</article>
{% endfor %}
</div>
<!-- Pagination für Meine Quizze -->
{% if my_quizzes.paginator.num_pages > 1 %}
<div class="flex justify-center space-x-2 mt-6 mb-8">
{% if my_quizzes.has_previous %}
<a href="?my_page={{ my_quizzes.previous_page_number }}&{{ querystring_my }}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</a>
{% endif %}
<span class="px-3 py-1 text-gray-600">Seite {{ my_quizzes.number }} von {{ my_quizzes.paginator.num_pages }}</span>
{% if my_quizzes.has_next %}
<a href="?my_page={{ my_quizzes.next_page_number }}&{{ querystring_my }}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
{% endif %}
</div>
{% endif %}
</div>
{% else %}
<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>
</div>
{% endif %}<script>
document.addEventListener('DOMContentLoaded', function () {
const filterPanel = document.getElementById('filterPanel');
const filterButton = document.getElementById('filterButton');
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('filter') === '1') {
filterPanel.classList.add('show');
}
filterButton.addEventListener('click', function () {
filterPanel.classList.toggle('show');
const params = new URLSearchParams(window.location.search);
if (filterPanel.classList.contains('show')) {
//params.set('filter', '1');
} else {
params.delete('filter');
}
history.replaceState(null, '', `${location.pathname}?${params}`);
});
});
document.addEventListener("DOMContentLoaded", function () {
// Scroll-Position beim Laden setzen
const scrollPos = localStorage.getItem("scroll-position");
if (scrollPos !== null) {
window.scrollTo(0, parseInt(scrollPos));
localStorage.removeItem("scroll-position");
}
// Nur Favoriten-Links abfangen
const favoriteLinks = document.querySelectorAll('a.favorite-link');
favoriteLinks.forEach(link => {
link.addEventListener('click', function () {
localStorage.setItem("scroll-position", window.scrollY);
});
});
});
</script>
{% endblock content %}

View File

@@ -0,0 +1,99 @@
{% 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">{% if question %}Frage bearbeiten{% else %}Neue Eingabe 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">
<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">Frage</label>
<div class="mt-1">
<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"
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="bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400" 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">Richtige Antwort</label>
<div class="flex space-x-4">
<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"
required>{{ option.value }}</textarea>
</div>
</div>
{% endfor %}
{% else %}
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Richtige Antwort</label>
<div class="flex space-x-4">
<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"
required></textarea>
</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">
<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">Credits</label>
<div class="mt-1">
<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"
>{{ 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>
{% endblock %}

View File

@@ -9,9 +9,8 @@
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">
<form method="post" class="space-y-6">
<form method="post" class="space-y-6" enctype="multipart/form-data">
{% csrf_token %}
<!-- Question Text -->
@@ -24,6 +23,19 @@
</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="bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400" type="file" name="question_image" id="question_image">
</div>
<!-- Options -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Antwortmöglichkeiten</label>
@@ -81,6 +93,38 @@
</svg>
</button>
</div>
<div class="flex items-center space-x-3">
<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"
required></textarea>
<label class="inline-flex items-center">
<input type="checkbox" name="correct_3" value="1"
class="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>
</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>
<div class="flex items-center space-x-3">
<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"
required></textarea>
<label class="inline-flex items-center">
<input type="checkbox" name="correct_4" value="1"
class="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>
</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>
{% endif %}
</div>
<button type="button" onclick="addOption()"
@@ -92,6 +136,25 @@
</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">
<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">Credits</label>
<div class="mt-1">
<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"
>{{ 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">

View File

@@ -11,7 +11,7 @@
</div>
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md">
<form method="post" class="space-y-6">
<form method="post" class="space-y-6" enctype="multipart/form-data">
{% csrf_token %}
<!-- Question Text -->
@@ -24,6 +24,18 @@
</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="bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400" type="file" name="question_image" id="question_image">
</div>
<!-- True/False Selection -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Richtige Antwort</label>
@@ -43,6 +55,26 @@
</div>
</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">
<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">Credits</label>
<div class="mt-1">
<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"
>{{ 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">

View File

@@ -1,26 +1,31 @@
<nav class="flex justify-between items-center bg-blue-600 h-12 px-4 sm:px-6 lg:px-8 rounded-lg m-2 shadow-md overflow-x-auto whitespace-nowrap">
<div class="flex items-center flex-shrink-0">
<nav id="main-nav" class="relative bg-blue-600 rounded-lg mt-1 m-2 shadow-md transition-all duration-300">
<!-- Desktop Navigation Bar -->
<div class="flex justify-between items-center h-12 px-4 sm:px-6 overflow-x-auto whitespace-nowrap">
<!-- Logo / Left section -->
<div class="flex items-center flex-shrink-0 md:w-32">
<h2 class="text-xl font-bold text-white hover:scale-110 transition duration-200">
<a href="{% url 'homepage:home' %}">qivip</a>
</h2>
</div>
{% if show_search %}
<form method="get" class="mr-2 ml-2 flex items-center w-full max-w-sm bg-white rounded-full px-4 py-1 shadow-md">
<input type="text" name="search" placeholder="Suche ..." value="{{ request.GET.search }}"
class="w-full px-3 py-1 text-black bg-transparent focus:outline-none">
<button type="submit" 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-6">
<!-- Desktop Search (centered on screen) - Hidden on mobile -->
<div class="hidden md:flex justify-center items-center absolute left-1/2 transform -translate-x-1/2 min-w-40">
<form method="get" action="{% url 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">
<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">
<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">
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
</svg>
</button>
</form>
{% endif %}
</div>
<div class="flex items-center space-x-4">
<!-- Desktop Navigation Links - Hidden on mobile -->
<div class="hidden md:flex items-center">
<ul class="flex space-x-4 qp-nav-list">
{% if show_search %}
<li class="hidden md:flex"><a href="{% url 'library:overview_quiz' %}">Bibliothek</a></li>
<li><a href="{% url 'library:overview_quiz' %}">Bibliothek</a></li>
{% else%}
<li><a href="{% url 'library:overview_quiz' %}">Bibliothek</a></li>
{% endif %}
@@ -31,4 +36,158 @@
{% endif %}
</ul>
</div>
<!-- Mobile menu button - Only visible on mobile -->
<div class="flex md:hidden items-center">
<button id="mobile-menu-button" class="text-white focus:outline-none">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
<!-- Mobile Navigation Menu - Hidden by default - FULLWIDTH DROPDOWN -->
<div id="mobile-menu" class="hidden absolute w-full z-50 md:hidden opacity-0 transform -translate-y-2 transition-all duration-300">
<div class="bg-blue-500 pt-3 pb-4 rounded-b-lg shadow-inner">
<!-- Search in mobile menu -->
<form method="get" action="{% url 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">
<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">
<button class="text-blue-600">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-5">
<path stroke-linecap="round" stroke-linejoin="round"
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
</svg>
</button>
</div>
<datalist id="quiz-names-desktop">
{% for quiz in quiz_names %}
<option value="{{ quiz.name }}">
{% endfor %}
</datalist>
<datalist id="quiz-names-mobile">
{% for quiz in quiz_names %}
<button value="{{ quiz.name }}" onclick="performSearch(this.value)"><option value="{{ quiz.name }}" ></option></button>
{% endfor %}
</datalist>
{{ quiz_names|json_script:"quiz-names-data" }}
<script>
async function fetchQuizNames() {
try {
const response = await fetch("{% url 'library:quiz_names_json' %}");
if (!response.ok) throw new Error("Netzwerkfehler");
const data = await response.json();
if (Array.isArray(data)) return data;
} catch (err) {
console.error("Fehler beim Laden der Quiz-Namen:", err);
}
return [];
}
function setupAutocomplete(inputId, datalistId, quizNames) {
const searchInput = document.getElementById(inputId);
const datalist = document.getElementById(datalistId);
if (!searchInput || !datalist) return;
// Wenn der Benutzer den Text ändert, Filter anwenden
searchInput.addEventListener('input', () => {
const value = searchInput.value.toLowerCase();
datalist.innerHTML = '';
quizNames
.filter(name => name.toLowerCase().includes(value))
.slice(0, 10)
.forEach(name => {
const option = document.createElement('option');
option.value = name;
datalist.appendChild(option);
});
});
}
// Funktion zum Durchführen der Suche
function performSearch(query) {
datalist.innerHTML = '';
// Hier kann deine Logik zur Verarbeitung der Suche eingefügt werden
// Falls du die Seite zu einer Suchergebnisseite weiterleiten möchtest:
window.location.href = `{% url 'library:overview_quiz' %}?search=${query}`;
}
document.addEventListener('DOMContentLoaded', async () => {
const quizNames = await fetchQuizNames();
setupAutocomplete('quiz-search-desktop', 'quiz-names-desktop', quizNames);
setupAutocomplete('quiz-search-mobile', 'quiz-names-mobile', quizNames);
});
</script>
</form>
<!-- Mobile Navigation Links -->
<div class="space-y-2 px-4">
<a href="{% url 'library:overview_quiz' %}" class="block py-2 text-white font-medium rounded-lg hover:bg-blue-600 transition duration-200">
Bibliothek
</a>
{% if user.is_authenticated %}
<a href="{% url 'accounts:home' %}" class="block py-2 text-white font-medium rounded-lg hover:bg-blue-600 transition duration-200">
Konto
</a>
{% else %}
<a href="{% url 'accounts:login' %}" class="block py-2 text-white font-medium rounded-lg hover:bg-blue-600 transition duration-200">
Anmelden
</a>
{% endif %}
</div>
</div>
</div>
</nav>
<!-- Mobile Menu Script -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const mobileMenuButton = document.getElementById('mobile-menu-button');
const mobileMenu = document.getElementById('mobile-menu');
const mainNav = document.getElementById('main-nav');
mobileMenuButton.addEventListener('click', function() {
// Toggle menu visibility
if (mobileMenu.classList.contains('hidden')) {
// Show menu
mobileMenu.classList.remove('hidden');
// Entferne unten abgerundete Ecken der Navbar wenn Menü offen
mainNav.classList.remove('rounded-lg');
mainNav.classList.add('rounded-t-lg');
// Animation einfahren
setTimeout(() => {
mobileMenu.classList.remove('opacity-0', 'transform', '-translate-y-2');
mobileMenu.classList.add('opacity-100');
}, 50);
} else {
// Ausfahren Animation
mobileMenu.classList.add('opacity-0', 'transform', '-translate-y-2');
mobileMenu.classList.remove('opacity-100');
// Nach der Animation verstecken
setTimeout(() => {
mobileMenu.classList.add('hidden');
// Stelle die abgerundeten Ecken wieder her
mainNav.classList.remove('rounded-t-lg');
mainNav.classList.add('rounded-lg');
}, 300);
}
});
});
</script>

View File

@@ -7,10 +7,11 @@
<div class="bg-white rounded-lg shadow-md p-8 mb-6">
<div class="text-center mb-8">
<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 Mitspielen!</p>
<p class="text-xl text-gray-600">Vielen Dank fürs Spielen!</p>
</div>
{% if not is_host %}
{% if rating_enabled %}
<div class="bg-blue-50 rounded-lg p-6 mb-8">
<h2 class="text-2xl font-bold text-blue-800 mb-4 text-center">Wie hat dir das Quiz gefallen?</h2>
<div class="stars flex justify-center space-x-4 mb-4">
@@ -22,6 +23,9 @@
</div>
<p id="rating-message" class="text-center text-blue-600 font-medium"></p>
</div>
{% else %}
<p class="text-center text-blue-600 font-medium mb-8">Bewertungen sind deaktiviert</p>
{% endif %}
{% endif %}
<div class="text-center">

View File

@@ -1,41 +1,119 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mx-auto px-4">
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<p class="text-gray-700 font-bold text-xl mb-4">Frage {{ question_index|add:1 }} von {{ total_questions }}</p>
<h1 class="text-3xl font-bold mb-6">{{ question_data.question }}</h1>
<div class="flex justify-end mb-4">
{% if request.session.mode == "learn" %}
<button onclick="leaveGame()" class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">
Spiel verlassen
</button>
{% 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">
weiter
</button>
</div>
<div id="form_host" class="bg-white rounded-lg shadow-md p-4 mb-6">
<div class="flex justify-between mb-2">
<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-sm">von</p>
<p class="text-gray-700 font-bold text-sm">{{ total_questions }}</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"></p></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>
{% if question_image %}
<div class="flex justify-center">
<img class="m-4 max-h-[33vh] w-auto rounded-lg" src="{{ question_image.image.url }}" alt="Bild der Frage">
</div>
{% endif %}
{% if request.session.mode != "learn" %}
<div class="w-full bg-gray-300 rounded-full h-4">
<div id="time-bar" class="bg-blue-600 h-4 rounded-full transition-all duration-100 linear" style="width: 100%;"></div>
</div>
{% endif %}
{% if request.session.mode == "learn" %}
<div class="grid grid-cols-1 gap-4 mb-6">
{% else %}
<div class="grid grid-cols-2 gap-4 mb-6">
<div class="p-4 bg-blue-50 rounded-lg">
<p class="text-blue-600 text-xl mb-2">Verbleibende Zeit</p>
<p id="remaining-time" class="text-6xl font-bold text-blue-700">30</p>
</div>
<div class="p-4 bg-blue-50 rounded-lg">
<p class="text-blue-600 text-xl mb-2">Antworten</p>
<p id="answer-count" class="text-6xl font-bold text-blue-700">0/0</p>
</div>
</div>
{% endif %}
<div class="grid grid-cols-2 gap-4">
{% for option in question_data.options %}
<div class="p-4 bg-gray-100 rounded-lg" data-correct="{{ option.is_correct }}">
<p class="text-lg">{{ option.value }}</p>
<p id="option-count-{{ forloop.counter0 }}" class="text-gray-600 hidden">0 Antworten</p>
</div>
{% if question_data.type != "input" %}
<div class="grid grid-cols-2 gap-4" id="options-container">
{% for option in question_data.options %}
<button
class="
{% 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 %}
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 }}"
{% if request.session.mode == "learn" %}
onclick="submitAnswer({{ forloop.counter0 }});" {% endif %}>
<p class="text-lg">{{ option.value }}</p>
</button>
{% endfor %}
</div>
{% else %}
{% if request.session.mode == "learn" %}
<form action="">
<input id="textanswer" type="text" placeholder="DEINE ANTWORT"
class="answer-option text-center p-2 bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 answer-option lg:w-80 ">
<button {% if request.session.mode == "learn" %}
onclick="submitAnswer( -1 );" {% endif %}
class="mt-2 text-center p-2 bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 answer-option">
abschicken
</button>
</form>
{% endif %}
{% endif %}
</div>
</div>
{% endblock %}
{% block extra_js %}
{% include 'play/game/common_scripts.html' %}
{% include 'play/game/sound.html' %}
<script>
const joinCode = '{{ quiz_game.join_code }}';
const hostId = '{{ host_id }}';
const questionStartTime = {{ start_time }};
const questionDuration = 30000; // 30 seconds in milliseconds
const questionDuration = '{{ current_question.time_per_question }}'*1000; //Zeit pro Frage (indviduell eingestellt)
const gameSocket = initializeGameSocket(joinCode);
let answeredParticipants = 0;
let totalParticipants = 0;
@@ -54,6 +132,7 @@
data.action === 'show_scores' &&
data.redirect_url) {
window.location.href = data.redirect_url;
submitAnswer( -1 );
}
};
@@ -81,11 +160,12 @@
}
}
function advanceToScores() {
// Show correct answers and answer counts
var correctOptions = document.querySelectorAll('[data-correct="True"]');
for (var i = 0; i < correctOptions.length; i++) {
correctOptions[i].classList.add('border-2', 'border-green-500');
correctOptions[i].classList.add('border-2', 'border-blue-500');
}
// Show answer counts
@@ -104,12 +184,18 @@
}
// Timer
{% if request.session.mode != "learn" %}
function updateTimer() {
const now = Date.now();
const elapsed = now - questionStartTime;
const remaining = Math.max(0, Math.ceil((questionDuration - elapsed) / 1000));
const remaining = Math.max(0, (questionDuration - elapsed) / 1000);
document.getElementById('remaining-time').textContent = remaining;
//document.getElementById('remaining-time').textContent = remaining;
document.getElementById('time-bar').style.width = ((remaining * 1000) / questionDuration * 100) + '%';
if (remaining<3){
document.getElementById('time-bar').style.backgroundColor="red";}else if(remaining<5){document.getElementById('time-bar').style.backgroundColor="orange";}
if (remaining === 0) {
advanceToScores();
@@ -118,12 +204,82 @@
}
}
{% endif %}
// Request initial participants list
gameSocket.onopen = function(e) {
gameSocket.send(JSON.stringify({
'type': 'update_participants'
}));
{% if request.session.mode != "learn" %}
updateTimer();
{% endif %}
};
let hasAnswered = false;
const participantId = '{{ host_id }}'; // HIER IST DER HOST DER SPIELER
function leaveGame() {
if (confirm('Möchtest du das Spiel wirklich verlassen?')) {
gameSocket.send(JSON.stringify({
type: 'leave_game',
participant_id: participantId
}));
window.location.href = '/';
gameSocket.close();
}
}
function submitAnswer(optionIndex) {
if (hasAnswered) return;
let input = ""; // Declare input here
try {
input = document.getElementById("textanswer").value;
} catch (e) {
input = "";
}
hasAnswered = true;
const now = Date.now();
let timeRemaining = Math.max(0, questionDuration - (now - questionStartTime));
{% if request.session.mode == "learn" %}
timeRemaining = questionDuration;
{% endif %}
// Disable all options and highlight selected
const options = document.getElementsByClassName('answer-option');
for (let option of options) {
option.disabled = true;
option.classList.remove('hover:bg-blue-100');
if (parseInt(option.dataset.index) === optionIndex) {
option.classList.remove('bg-gray-100');
option.classList.add('bg-blue-600', 'text-white');
} else {
option.classList.add('opacity-50');
}
}
if(input!=""){
// Send answer to server
gameSocket.send(JSON.stringify({
type: 'submit_answer',
participant_id: participantId,
answer:input,
time_remaining: timeRemaining
}));
}
else{
gameSocket.send(JSON.stringify({
type: 'submit_answer',
participant_id: participantId,
answer:optionIndex,
time_remaining: timeRemaining
}));
}
}
</script>
{% endblock %}

View File

@@ -8,24 +8,65 @@
</button>
</div>
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<p class="text-gray-700 font-bold text-xl mb-4">Frage {{ question_index|add:1 }} von {{ total_questions }}</p>
<h1 class="text-3xl font-bold mb-6">{{ question_data.question }}</h1>
<div class="p-4 bg-blue-50 rounded-lg mb-6">
<p class="text-blue-600 text-xl mb-2">Verbleibende Zeit</p>
<p id="remaining-time" class="text-6xl font-bold text-blue-700">30</p>
<div class="flex justify-between mb-2">
<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-sm">von</p>
<p class="text-gray-700 font-bold text-sm">{{ total_questions }}</p>
</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>
<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>
{% if question_data.type != "input" %}
<div class="grid grid-cols-2 gap-4" id="options-container">
{% for option in question_data.options %}
<button
class="p-4 bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 answer-option"
class="
{% 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 %}
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 }}"
onclick="submitAnswer({{ forloop.counter0 }});">
<p class="text-lg">{{ option.value }}</p>
</button>
{% endfor %}
</div>
{% else %}
<form action="">
<input id="textanswer" type="text" placeholder="DEINE ANTWORT"
class="answer-option text-center p-2 bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 answer-option lg:w-80">
<button
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">
abschicken
</button>
</form>
{% endif %}
</div>
</div>
{% endblock %}
@@ -36,8 +77,8 @@
let hasAnswered = false;
const joinCode = '{{ quiz_game.join_code }}';
const participantId = '{{ participant.participant_id }}';
const questionStartTime = {{ start_time }};
const questionDuration = 30000; // 30 seconds in milliseconds
const questionStartTime = Date.now();
const questionDuration = '{{ current_question.time_per_question }}'*1000; //Zeit pro Frage (indviduell eingestellt)
const gameSocket = initializeGameSocket(joinCode);
gameSocket.onmessage = function(e) {
@@ -48,6 +89,7 @@
data.action === 'show_scores' &&
data.redirect_url) {
window.location.href = data.redirect_url;
submitAnswer( -1 );
}
};
@@ -57,6 +99,13 @@
function submitAnswer(optionIndex) {
if (hasAnswered) return;
let input = ""; // Declare input here
try {
input = document.getElementById("textanswer").value;
} catch (e) {
input = "";
}
hasAnswered = true;
const now = Date.now();
@@ -75,22 +124,38 @@
}
}
if(input!=""){
// Send answer to server
gameSocket.send(JSON.stringify({
type: 'submit_answer',
participant_id: participantId,
answer: optionIndex,
answer:input,
time_remaining: timeRemaining
}));
}
else{
gameSocket.send(JSON.stringify({
type: 'submit_answer',
participant_id: participantId,
answer:optionIndex,
time_remaining: timeRemaining
}));
}
}
// Timer
function updateTimer() {
const now = Date.now();
const elapsed = now - questionStartTime;
const remaining = Math.max(0, Math.ceil((questionDuration - elapsed) / 1000));
const remaining = Math.max(0, (questionDuration - elapsed) / 1000);
document.getElementById('time-bar').style.width = ((remaining * 1000) / questionDuration * 100) + '%';
if (remaining<3){
document.getElementById('time-bar').style.backgroundColor="red";}else if(remaining<5){document.getElementById('time-bar').style.backgroundColor="orange";}
document.getElementById('remaining-time').textContent = remaining;
//document.getElementById('remaining-time').textContent = remaining;
if (remaining === 0 && !hasAnswered) {
// Auto-submit timeout answer

View File

@@ -10,9 +10,15 @@
<p class="text-lg mb-2">{{ question_data.question }}</p>
<div class="grid grid-cols-2 gap-4">
{% for option in question_data.options %}
<div class="p-4 rounded-lg {% if option.is_correct %}bg-green-100 border-2 border-green-500{% else %}bg-gray-100{% endif %}">
<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-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">
<p class="text-lg">{{ option.value }}</p>
<p class="text-gray-600" id="option-count-{{ forloop.counter0 }}">0 Antworten</p>
</div>
</div>
{% endfor %}
</div>
@@ -20,30 +26,130 @@
<div class="mb-6">
<h2 class="text-xl font-bold mb-4">Punktestand</h2>
<div class="space-y-2">
<div id="scoreboard" class="leading-relaxed">
{% for participant in participants %}
<div class="p-4 {% if forloop.first %}bg-yellow-100 border-2 border-yellow-500{% else %}bg-gray-100{% endif %} rounded-lg flex justify-between items-center">
<div>
<p class="text-lg font-bold">{{ participant.display_name }}</p>
<p class="text-gray-600">{{ participant.score }} Punkte</p>
</div>
{% if participant.last_answer_correct %}
<span class="text-green-500"></span>
{% elif participant.last_answer_correct == False %}
<span class="text-red-500"></span>
{% 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="flex flex-col items-start gap-1">
<div class="text-lg 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-2xl " 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="text-gray-600 font-bold show-score" id="score-{{ forloop.counter0 }}">{{ participant.score }} Punkte</p>
{% if participant == my_participant or request.session.mode == "learn"%}
<p class="text-blue-600 font-bold text-sm">+{{ participant.last_score }} Punkte</p>
{% endif %}
</div>
<script>
async function start() {
elements=document.getElementsByClassName("show-score");
for (let i = 0; i < elements.length; i++) {
const el = elements[i];
let counter=Number(el.getAttribute('data-score'))-Number(el.getAttribute('data-last-score'));
let goal=Number(el.getAttribute('data-score'));
let interval= setInterval(function(){
el.innerHTML = counter + " Punkte";
if(counter<=goal){
el.innerHTML = counter + " Punkte";
counter+=8;
}
else{
el.innerHTML = goal + " Punkte";
clearInterval(interval);
}
}, 20);}}
function bounce(){
const names = document.getElementsByClassName("display_name");
for (let i = 0; i < names.length; i++) {
names[i].classList.add("animate-bounce");
}
}
{% if is_last_question %}
bounce();
{% else %}
start();
{% endif %}
</script>
{% if participant == my_participant or request.session.mode == "learn" %}
{% if participant.last_answer_correct %}
<div class="mt-1 flex justify-center border-t-2 border-gray-300 gap-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="pt-2 text-center text-green-500 font-black size-10">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<div class=" pt-3 text-center text-gray-600 font-bold" id="antwort-richtig">
Hier erscheint gleich dein Feedback...
</div>
</div>
{% 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="w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
<button id="finish-button" class="mt-4 w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
Quiz beenden
</button>
{% else %}
<button id="next-button" class="w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
<button id="next-button" class="mt-4 w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
Nächste Frage
</button>
{% endif %}
@@ -53,8 +159,144 @@
{% endblock %}
{% block extra_js %}
{% include 'play/game/common_scripts.html' %}
{% 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 = [
"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';
@@ -102,21 +344,30 @@
'type': 'next_question',
'host_id': hostId
}));
});
}, { once: true });
}
{% endif %}
{% endif %}
function updateAnswerStats(stats) {
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 element = document.getElementById('option-count-' + optionIndex);
if (element) {
element.textContent = stats[optionIndex] + ' Antworten';
}
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) {

View File

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

View File

@@ -7,7 +7,7 @@
<form action="{% url 'play:join_game' %}" method="post" class="flex flex-col gap-4">
{% csrf_token %}
<div class="flex gap-3 items-center justify-center">
<input type="text" 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" 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>
<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">

View File

@@ -7,11 +7,18 @@
<!-- Quiz Info -->
<div class="text-center mb-8">
<h3 class="text-xl text-gray-600 mb-2">{{ quiz.name }}</h3>
{% if request.session.mode != "learn" %}
{% if host_id %}
<div class="bg-blue-100 rounded-lg p-4 mb-4">
<h1 class="text-4xl font-bold text-blue-800">{{ quiz_game.join_code }}</h1>
<div class="w-full flex justify-center my-2">
<img src="data:image/png;base64,{{ qr_code_base64 }}" class="h-1/2 w-1/2" alt="QR-Code">
</div>
<p class="text-sm text-blue-600 mt-1">Spiel-Code</p>
</div>
</div>
{% endif %}
{% endif %}
<!-- Participant Info -->
{% if participant %}
@@ -24,14 +31,14 @@
{% endif %}
<!-- Participants List -->
<div class="mb-6">
<div class="mb-6 overflow-y-scroll max-h-96">
<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">
<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>
Teilnehmer
</h3>
<ul id="participants-list" class="space-y-2 max-h-60 overflow-y-auto">
<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>
</ul>
</div>

View File

@@ -0,0 +1,41 @@
{% extends 'base.html' %}
{% block content %}
<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">
<span class="flex justify-center font-bold p-4 text-center text-1xl lg:text-2xl">Bitte wählen Sie einen Spielmodus aus:</span>
<div class="grid sm:grid-cols-2 place-items-stretch text-center gap-4 p-4">
<!-- Moderieren -->
<a class="qp-a-button bg-green-500 text-white flex justify-center items-center relative group h-20 gap-2" href="{% url 'play:selected_mode' join_code %}?mode=classic">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z" />
</svg>
Moderiermodus
<!-- Tooltip -->
<span class="absolute w-full h-1/2 top-1/4 left-0 hidden group-hover:flex items-center justify-center text-white bg-black bg-opacity-90 text-xs rounded px-2 z-10 pointer-events-none">
Klassischer Spielmodus zum Moderieren
</span>
</a>
<!-- Lernmodus -->
<a class="qp-a-button bg-indigo-500 text-white flex justify-center items-center relative group h-20 gap-2" href="{% url 'play:selected_mode' join_code %}?mode=learn">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
</svg>
Lernmodus
<!-- Tooltip -->
<span class="absolute w-full h-1/2 top-1/4 left-0 hidden group-hover:flex items-center justify-center text-white bg-black bg-opacity-90 text-xs rounded px-2 z-10 pointer-events-none">
Modus zum selbständigen Üben und Lernen
</span>
</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,35 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mx-auto px-4">
<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="text-center">
<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"/>
</svg>
<h2 class="mt-4 text-xl font-bold text-gray-900">Löschen bestätigen</h2>
<p class="mt-2 text-sm text-gray-600">
Möchten Sie ihren Account wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.
</p>
</div>
<form method="post" class="mt-6">
{% csrf_token %}
<div class="flex justify-center space-x-3">
<a href="{% url 'accounts:home' %}"
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-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
Löschen
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,42 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Passwort ändern{% endblock %}
{% block content %}
<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">
<h2 class="text-2xl text-center p-4 font-black">Passwort ändern</h2>
<form class="space-y-4" method="post">
{% if form.errors %}
<div class=" items-center text-red-600 font-black overflow-x-auto break-words text-center">
<ul>
{% for field, errors in form.errors.items %}
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
</div>
{% endif %}
{% csrf_token %}
<div class="register">
{{ form.old_password.label_tag }}
<input type="password" name="old_password" placeholder="ALTES PASSWORT" />
</div>
<div class="register">
{{ form.new_password1.label_tag }}
<input type="password" name="new_password1" placeholder="NEUES PASSWORT" />
</div>
<div class="register">
{{ form.new_password2.label_tag }}
<input type="password" name="new_password2" placeholder="NEUES PASSWORT WIEDERHOLEN" />
</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="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>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Passwort zurückgesetzt{% endblock %}
{% block content %}
<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">
<h2 class="text-2xl text-center p-4 font-black">Passwort zurückgesetzt</h2>
<div class="space-y-4">
Das Passwort wurde erfolgreich zurückgesetzt! Sie können sich nun mit dem neuen Passwort anmelden!
{% csrf_token %}
<div class="mt-4">
<a href="{% url 'accounts:login' %}" class="register_link"><button class="w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200" class="login-button">anmelden</button></a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,42 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Passwort zurücksetzen{% endblock %}
{% block content %}
<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">
<h2 class="text-2xl text-center p-4 font-black">Passwort zurücksetzen</h2>
{% if validlink %}
<form class="space-y-4" method="post">
{% if form.errors %}
<div class=" items-center text-red-600 font-black overflow-x-auto break-words text-center">
<ul>
{% for field, errors in form.errors.items %}
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
</div>
{% endif %}
{% csrf_token %}
<div class="register">
{{ form.new_password1.label_tag }}
<input type="password" name="new_password1" placeholder="NEUES PASSWORT" />
</div>
<div class="register">
{{ form.new_password2.label_tag }}
<input type="password" name="new_password2" placeholder="NEUES PASSWORT WIEDERHOLEN" />
</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="text-center text-sm w-full mt-2 font-light text-blue-600"><a href="{% url 'accounts:home' %}" class="register_link">Doch nicht zurücksetzen?</a></button>
</form>
{% else %}
Dieser Link ist leider nicht mehr gültig oder abgelaufen. Bitte versuchen Sie es erneut:
<button class="text-center text-sm w-full mt-2 font-light text-blue-600"><a href="{% url 'accounts:password_reset' %}" class="register_link">Passwort zurücksetzen</a></button>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}E-Mail gesendet{% endblock %}
{% block content %}
<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">
<h2 class="text-2xl font-black mb-4">E-Mail wurde gesendet</h2>
<p class="text-gray-700 mb-6">
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>
<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>
</div>
</div>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More