aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2026-01-23 18:11:38 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2026-01-23 18:13:57 +0100
commit81acf4bb41d4d9297d1dcb9baa901b5bb0c3975e (patch)
tree43ee97784d50b2aeb8f66736128ced8912825b4e
parentde186f81e3e84f706e79e3623cbc6226ebdb12fe (diff)
downloadpunktestand-81acf4bb41d4d9297d1dcb9baa901b5bb0c3975e.tar.gz
punktestand-81acf4bb41d4d9297d1dcb9baa901b5bb0c3975e.zip
füge eine einfache kuchenstand Webseite hinzu
Die kuchenstand Webseite betrachtet nur die konfigurierten Streak-Ereignisse und stellt diese als prozentualen Kuchen dar. TODO: Mache das Streak-Symbolbild konfigurierbar.
-rw-r--r--Makefile13
-rw-r--r--Readme.md7
-rw-r--r--img/kuchen.pngbin0 -> 377582 bytes
-rw-r--r--kuchenstand.css40
-rw-r--r--kuchenstand.js202
-rw-r--r--kuchenstand.php78
6 files changed, 336 insertions, 4 deletions
diff --git a/Makefile b/Makefile
index 00d352a..5658648 100644
--- a/Makefile
+++ b/Makefile
@@ -1,11 +1,12 @@
JS := node_modules/chart.js/dist/chart.umd.js node_modules/chart.js/dist/chart.umd.js.map \
node_modules/chartjs-plugin-annotation/dist/chartjs-plugin-annotation.min.js \
- index.js
-PHP := index.php lade_daten.php config.php
-CSS := index.css punktestand.css
+ index.js kuchenstand.js
+PHP := index.php lade_daten.php config.php kuchenstand.php
+CSS := index.css punktestand.css kuchenstand.css
+IMGS := img/kuchen.png
.PHONY: all
-all: dist/js dist/php dist/css
+all: dist/js dist/php dist/css dist/img
dist:
mkdir -p $@
@@ -14,6 +15,10 @@ dist/js: $(JS) dist
mkdir -p $@
for js in $(JS); do cp $$js $@; done
+dist/img: $(IMGS) dist
+ mkdir -p $@
+ for img in $(IMGS); do cp $$img $@; done
+
.PHONY: dist/php
dist/php: $(PHP) dist
for php in $(PHP); do cp $$php ${@D}; done
diff --git a/Readme.md b/Readme.md
index 341d65e..a0771e1 100644
--- a/Readme.md
+++ b/Readme.md
@@ -117,3 +117,10 @@ PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
Punktestand. If not, see <https://www.gnu.org/licenses/>.
+
+### Kuchenbild
+
+Das Kuchenbild der kuchenstand Webseite ist unter der CC BY-NC 4.0 Lizenz unter
+[1] veröffentlicht.
+
+[1]: https://pngimg.com/image/13101
diff --git a/img/kuchen.png b/img/kuchen.png
new file mode 100644
index 0000000..6aa0188
--- /dev/null
+++ b/img/kuchen.png
Binary files differ
diff --git a/kuchenstand.css b/kuchenstand.css
new file mode 100644
index 0000000..02b7539
--- /dev/null
+++ b/kuchenstand.css
@@ -0,0 +1,40 @@
+#kuchen-wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin: auto;
+ max-width: 50%;
+}
+
+img.kuchen-hintergrund {
+ position: relative;
+ max-width: 100%;
+ opacity: 0.4;
+}
+
+img.kuchen-vordergrund {
+ position: relative;
+}
+
+#streak {
+ position: relative;
+}
+
+.klassen-navigation {
+ list-style-type: none;
+ margin: auto;
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+ul.klassen-navigation li {
+ display: block;
+ padding: 14px 16px;
+ text-decoration: none;
+}
+
+ul li:hover {
+ background-color: #111111;
+}
diff --git a/kuchenstand.js b/kuchenstand.js
new file mode 100644
index 0000000..a3b8949
--- /dev/null
+++ b/kuchenstand.js
@@ -0,0 +1,202 @@
+/**
+ *
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page.
+ *
+ * Copyright (C) 2025-2026 Florian Fischer
+ *
+ *
+ * The JavaScript code in this page is free software: you can
+ * redistribute it and/or modify it under the terms of the GNU
+ * General Public License (GNU GPL) as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option)
+ * any later version. The code is distributed WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
+ *
+ * As additional permission under GNU GPL version 3 section 7, you
+ * may distribute non-source (e.g., minimized or compacted) forms of
+ * that code without the copy of the GNU GPL normally required by
+ * section 4, provided you include this license notice and a URL
+ * through which recipients can access the Corresponding Source.
+ *
+ * @licend The above is the entire license notice
+ * for the JavaScript code in this page.
+ *
+ */
+
+// Observer der auf 'resize' events unseres Kuchens reagiert und den zweiten
+// Kuchen und die Streak-Anzeige gegebenfalls anpasst.
+let kuchenResizeObserver;
+
+// Berechne die Streaks, wenn die Seite komplett geladen wurde.
+window.onload = function() {
+ berechneStreaks();
+ const params = new URLSearchParams(location.search);
+ // Ist in der URL die Klasse per Search-Parameter ?klasse=... spezifiziert?
+ const klasse = params.get('klasse');
+ if (klasse) {
+ wechselKlasse(klasse);
+ }
+}
+
+var streaks;
+
+// Berechne den Streak für alle Klassen.
+// Die globale Variable daten.streakEreignisse muss definiert sein und ein Iterable
+// aller Ereignisse, die für den Streak relevant sind, enthalten.
+// Ereignisse ohne positives Delta beenden einen Streak.
+function berechneStreaks() {
+ // Es gibt keine Streak-Ereignisse
+ if (!daten.streakEreignisse) {
+ return;
+ }
+
+ // Variable um den laufenden Streak per Klasse zu bestimmen
+ streaks = Object.fromEntries(daten.klassen.map(klasse => [klasse.bezeichner, 0]));
+
+ for (let ereignis of daten.streakEreignisse) {
+ const klasse = `${ereignis.Jahrgang}${ereignis.Bezeichner}`;
+
+ // Das Streak Ereignis bringt keine Punkte und unterbricht damit den Streak.
+ if (ereignis.Delta == 0) {
+ streaks[klasse] = 0;
+ // Erhöhe den Streak.
+ } else {
+ streaks[klasse] = streaks[klasse] + 1;
+ }
+ }
+}
+
+
+// Wechsel zur Ansicht der Klasse.
+function wechselKlasse(klasse) {
+ zeichne(klasse);
+ zeichneStreak(klasse);
+ listeStreakEreignisse(klasse);
+}
+
+// Zeichne die Kuchen Graphik für die aktuelle Klasse.
+function zeichne(klasse) {
+ // Wir haben keine Daten zum Zeichnen
+ if (!streaks) {
+ return;
+ }
+
+ const kuchenNode = document.getElementById("kuchen");
+ // Entferne alle bestehenden Kind-Objekte vom Kuchen-Element.
+ while (kuchenNode.firstChild) {
+ kuchenNode.removeChild(kuchenNode.firstChild);
+ }
+
+ const kuchenHintergrund = document.createElement("IMG");
+ kuchenHintergrund.src = "img/kuchen.png"
+ kuchenHintergrund.classList.add("kuchen-hintergrund");
+ // Füge das Hintergrundbild dem DOM hinzu
+ kuchenNode.appendChild(kuchenHintergrund);
+
+ const kuchenVordergrund = document.createElement("IMG");
+ kuchenVordergrund.src = "img/kuchen.png"
+ kuchenVordergrund.classList.add("kuchen-vordergrund");
+
+ // Passe die Größe des Kuchens anhand des Streaks an
+ const streak = streaks[klasse];
+ const proportion = streak/streak_anzahl * 100;
+ kuchenVordergrund.style.maxWidth = `${proportion}%`;
+ kuchenVordergrund.style.maxHeight = `${proportion}%`;
+
+ kuchenNode.appendChild(kuchenVordergrund);
+
+ // Erzeuge ein Observer der auf Größenveränderungen des Kuchens reagiert.
+ let streakNode = document.getElementById("streak");
+ kuchenResizeObserver = new ResizeObserver((entries) => {
+ for (const entry of entries) {
+ // Positioniere den kleinen Kuchen und die Streak-Anzeige passend zur
+ // echten Anzeigegröße.
+ kuchenVordergrund.style.top = `-${entry.contentRect.height/2 + kuchenVordergrund.offsetHeight/2}px`;
+ kuchenVordergrund.style.left = `${entry.contentRect.width/2 - kuchenVordergrund.offsetWidth/2}px`;
+ streakNode.style.top = `-${kuchenVordergrund.offsetHeight}px`
+ }
+ });
+
+ kuchenResizeObserver.observe(kuchenHintergrund);
+}
+
+// Zeichne den Streak für die aktuelle Klasse.
+function zeichneStreak(klasse) {
+ // Es gibt keine Streak-Ereignisse
+ if (!streaks) {
+ return;
+ }
+
+ const streak = streaks[klasse];
+
+ let streakNode = document.getElementById("streak");
+ // Schreibe den aktuellen Streak in das Streak span-Element
+ streakNode.innerHTML = `${streak}/${streak_anzahl}`;
+}
+
+// Erzeuge eine Liste aller Ereignisse der Klasse.
+// Die globale Variable daten.streakEreignisse muss definiert sein und ein Iterable aller
+// Ereignisse, die angezeigt werden sollen, enthalten.
+function listeStreakEreignisse(klasse) {
+ let liste = document.getElementById("ereignisse");
+ while (liste.firstChild) {
+ liste.removeChild(liste.firstChild);
+ }
+
+ // Erzeuge ein Mapping von Klassenbezeichner auf die entsprechende Klasse
+ let klassen = {};
+ daten.klassen.forEach(klasse => { klassen[klasse.bezeichner] = klasse });
+
+ for (let ereignis of daten.streakEreignisse) {
+ let klassenBezeichner = `${ereignis.Jahrgang}${ereignis.Bezeichner}`
+
+ // Das Ereignis ist nicht für diese Klasse
+ if (klassenBezeichner !== klasse) {
+ continue;
+ }
+
+ // Erzeuge ein Listenelement, das alle Infos des Ereignisses enthält
+ let ereignisNode = document.createElement("li");
+ ereignisNode.classList.add("ereignis");
+
+ // Setze den Hintergrund des Ereignisses auf 40% der Klassenfarbe
+ const farbe = klassen[klassenBezeichner].farbe;
+ if (klasse) {
+ ereignisNode.style.backgroundColor = `${farbe}40`;
+ }
+
+ // Span, das die Meta-Informationen (Klasse, Datum) eines Ereignisses enthält
+ let ereignisUrsache = document.createElement("span");
+ ereignisUrsache.innerHTML = `<b>${klassenBezeichner}</b>`
+ if (ereignis.Datum) {
+ ereignisUrsache.innerHTML += ` <small>${ereignis.Datum}</small>`
+ }
+ ereignisUrsache.classList.add("ereignis-klasse");
+ ereignisNode.appendChild(ereignisUrsache);
+
+ // Die Veränderung des Punktestands durch das Ereignis
+ let ereignisDelta = document.createElement("span");
+ ereignisDelta.innerHTML = `${ereignis.Delta}`;
+ ereignisDelta.classList.add("ereignis-delta");
+ // Ergänze positive und negative CSS Klassen
+ if (ereignis.Delta > 0) {
+ ereignisDelta.classList.add("positiv");
+ } else {
+ ereignisDelta.classList.add("negativ");
+ }
+ ereignisNode.appendChild(ereignisDelta);
+
+ // Die Beschreibung des Ereignisses
+ let ereignisBeschreibung = document.createElement("span");
+ ereignisBeschreibung.innerHTML = `<i>${ereignis.Beschreibung}</i>`;
+ ereignisBeschreibung.classList.add("ereignis-beschreibung");
+ ereignisNode.appendChild(ereignisBeschreibung);
+
+ // Füge das neue Ereignis-Listenelement der Ereignis-Liste hinzu.
+ // Jedes neue Element wird als erstes Kind eingefügt, damit
+ // neuere Ereignisse weiter oben in der Liste stehen
+ liste.insertBefore(ereignisNode, liste.firstChild);
+ }
+}
diff --git a/kuchenstand.php b/kuchenstand.php
new file mode 100644
index 0000000..fb41f96
--- /dev/null
+++ b/kuchenstand.php
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" lang="de" xml:lang="de">
+<head>
+ <!-- Willkommen im Quellcode dieser Seite! Wenn du verstehen willst wie die
+ Seite funktioniert, findest du den Quellcode unter:
+ https://git.muhq.space/schule/punktestand
+ -->
+ <title>Kuchenstand</title>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="author" content="Florian Fischer" />
+
+ <!-- Setze ein leeres Bild als favicon -->
+ <link rel="icon" href="data:,">
+
+ <!-- Lade den allgemeinen CSS Code, der das Aussehen der Seite definiert -->
+ <link rel="stylesheet" href="index.css"/>
+ <!-- Lade den CSS Code, der das Aussehen der punktestand Seite geneauer definiert -->
+ <link rel="stylesheet" href="kuchenstand.css"/>
+
+ <!-- Die Daten für den Jahrgang und das Schuljahr aus der Datenbank -->
+ <?php
+ // Lade die Daten für den Jahrgang und das Schuljahr aus der Datenbank
+ $QUERIES = array();
+ parse_str($_SERVER['QUERY_STRING'], $QUERIES);
+
+ // TODO: Berechne das aktuelle Schuljahr
+ function aktuelles_schuljahr() {
+ return "2526";
+ }
+
+ $SCHULJAHR = $QUERIES['schuljahr'] ?? aktuelles_schuljahr();
+ $JAHRGANG = $QUERIES['jahrgang'];
+ // TODO: Behandle fehlenden jahrgangs URL query Parameter
+
+ // Lade die lokale Konfigurationsdatei, die die globale Variable $CONFIG definiert
+ require_once('config.php');
+
+ // Lade die Funktion, um die Daten aus der Datenbank zu lesen
+ require('lade_daten.php');
+
+ // Lade alle Daten aus der Datenbank und schreibe sie als Javbascript Variablen
+ // in die Webseite mit der in lade_daten.php definierten Funktion.
+ $daten = schreibe_json($CONFIG, $SCHULJAHR, $JAHRGANG);
+ ?>
+
+ <!-- Lade die benötigten Javascript Dateien -->
+ <script src="js/kuchenstand.js"></script>
+</head>
+
+<body>
+ <h1>Kuchenstand Jahrgangsstufe <?php echo "$JAHRGANG $SCHULJAHR";?></h1>
+
+ <ul class="klassen-navigation">
+ <?php
+ foreach($daten["klassen"] as $klasse) {
+ echo "<li onclick=\"wechselKlasse('$klasse->bezeichner');\">$klasse->bezeichner</li>";
+ }
+ ?>
+ </ul>
+
+ <!-- Container für den Inhalt des Punktestands -->
+ <div id="content">
+ <div id="kuchen-wrapper">
+ <div id="kuchen">
+ </div>
+ <span id="streak">Wähle eine Klasse aus</span>
+ </div>
+
+ <!-- Liste aller Ergeignisse -->
+ <div class="ereignisse-wrapper">
+ <h2>Ereignisse</h2>
+ <ul id="ereignisse">
+ </ul>
+ </div>
+ </div>
+</body>
+</html>