Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| adb5646b5e | |||
| b88962da6f | |||
| 55a09cb093 | |||
| 4599dd12b8 | |||
| bbe24c67eb | |||
| b4d71d7d59 | |||
| e26a0c059c | |||
| bf27c9ccdc | |||
| 0e4645541d | |||
| 477d4dc04a | |||
| 34b050503d | |||
| 83929f24ba | |||
| fc98e5aad1 | |||
| b3a0fde104 | |||
| 7cb6621d1b | |||
| 7ede78d984 | |||
| 9022961762 |
@@ -0,0 +1,11 @@
|
||||
.git
|
||||
target
|
||||
uploads
|
||||
*.log
|
||||
.env
|
||||
.env.*
|
||||
.claude
|
||||
.idea
|
||||
.settings
|
||||
.classpath
|
||||
.project
|
||||
@@ -0,0 +1,2 @@
|
||||
/mvnw text eol=lf
|
||||
*.cmd text eol=crlf
|
||||
@@ -21,6 +21,9 @@ jobs:
|
||||
docker push localhost:5000/mindflux:latest
|
||||
|
||||
- name: Deploy
|
||||
env:
|
||||
MINDFLUX_ADMIN_PASSWORD: ${{ secrets.MINDFLUX_ADMIN_PASSWORD }}
|
||||
MINDFLUX_ADMIN2_PASSWORD: ${{ secrets.MINDFLUX_ADMIN2_PASSWORD }}
|
||||
run: |
|
||||
docker compose -f docker-compose.yml pull
|
||||
docker compose -f docker-compose.yml up -d
|
||||
|
||||
+36
-41
@@ -1,45 +1,40 @@
|
||||
# ---> Java
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
|
||||
# ---> Maven
|
||||
HELP.md
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
uploads/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.log
|
||||
.claude/
|
||||
hs_err_pid*.log
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
# Eclipse m2e generated files
|
||||
# Eclipse Core
|
||||
.project
|
||||
# JDT-specific (Eclipse Java Development Tools)
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
# Changelog
|
||||
|
||||
Alle nennenswerten Änderungen an **Mindflux** werden hier dokumentiert.
|
||||
Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/),
|
||||
Versionierung [semantisch](https://semver.org/lang/de/).
|
||||
|
||||
## [Unreleased] – 2026-06-30 · Dev Lab
|
||||
|
||||
Neues Spiel **Dev Lab**: ein Entwicklerlabor, das mehrere kleine Disziplinen bündelt
|
||||
(SQL, Java/Debugging, Git, HTML, CSS, Regex, PHP, REST). Im Gegensatz zur ersten,
|
||||
rein clientseitigen Skizze ist Dev Lab jetzt **server-autoritativ, DB-gestützt und im
|
||||
Admin pflegbar** – mit **echter Mechanik-Vielfalt** statt überall demselben Lückentext.
|
||||
|
||||
### Hinzugefügt
|
||||
- **Spiel: Dev Lab (`/devlab`)** mit Hub (drei Trainingspfade: Syntax · Debugging · Praxis)
|
||||
und **11 Disziplinen** (`query-builder`, `sql-detective`, `debug-hunt`, `git-mission`,
|
||||
`html-architect`, `css-designer`, `css-cascade`, `flexbox-factory`, `regex-master`,
|
||||
`php-request`, `api-explorer`), zusammen **189 Aufgaben** (16–21 je Spiel).
|
||||
- **Sechs unterschiedliche Spielmechaniken** – passend zum Thema gemischt:
|
||||
**cloze** (Code-Lücke tippen), **choice** (Multiple-Choice-Chips), **order**
|
||||
(Bausteine klick-basiert zusammensetzen/sortieren), **spot** (den fehlerhaften Token
|
||||
anklicken), **match** (Paare zuordnen) und **regex** (Muster schreiben mit
|
||||
**Live-Treffer-Vorschau** gegen Beispiel-Strings).
|
||||
- **Server-autoritativ & Anti-Cheat:** Aus dem JSON-Payload jeder Aufgabe baut der Server
|
||||
eine bereinigte Sicht **ohne Lösung**; geprüft und gewertet wird ausschließlich serverseitig
|
||||
(Text-/Reihenfolge-/Verhaltensvergleich). `correctIndex`/`bugIndex`/Lösungen/Pattern verlassen
|
||||
nie das Backend. Score über die generische `game_scores`-Tabelle (Kennung `devlab`, `deck`=Spiel-Slug).
|
||||
- **Fehler-Mechanik** wie bei Syntax Saga: bis zu drei Versuche mit Hinweis, dann „Auflösung",
|
||||
kein Game-Over; Punkte nach Versuch, Sterne 0–3.
|
||||
- **Admin-Verwaltung (`/admin/devlab`):** Spiele und Aufgaben (Typ + JSON-Payload) anlegen,
|
||||
bearbeiten, löschen, aktiv/inaktiv schalten. Inhalte aus `devlab/content.json` (idempotenter Seeder).
|
||||
|
||||
### Behoben / geändert (ggü. der ersten Dev-Lab-Skizze)
|
||||
- **Platzhalter-Leak entfernt:** Die Eingabefelder zeigten vorher die Lösung als Platzhalter –
|
||||
jetzt sind es neutrale Kategorie-Labels („Keyword", „Operator", „Tag" …). Geprüft wird serverseitig.
|
||||
- **Echte Mechanik-Abwechslung** statt 11×Lückentext; **deutlich mehr Aufgaben** je Disziplin.
|
||||
|
||||
### Technik
|
||||
- Neue Entities `DevLabGame`/`DevLabTask` (JSON-`payload`) + Flyway `V8__devlab.sql`;
|
||||
`DevLabService` mit sechs Mechanik-Checkern; JSON-API `/api/devlab/**`
|
||||
(`catalog`/`session`/`check`/`finish`, CSRF-ausgenommen, `devlab` in `KNOWN_GAMES`).
|
||||
Frontend: polymorphe Engine `devlabgame.js`, DB-getriebener Hub `devlab.js`, `devlabgame.css` (Prefix `dlg-`).
|
||||
|
||||
## [Unreleased] – 2026-06-30
|
||||
|
||||
Neues Spiel **Syntax Saga**: interaktive IT-Geschichten, in denen man den fehlenden
|
||||
Code (Schlüsselwörter, Operatoren, ganze Zeilen) in eine kleine Story tippt. Server-autoritativ
|
||||
nach dem TechTacular-Muster, Inhalte DB-gestützt und im Admin pflegbar. Die Dashboard-Kachel
|
||||
„Syntax Saga" ist damit aktiv (vorher „coming soon").
|
||||
|
||||
### Hinzugefügt
|
||||
- **Spiel: Syntax Saga (`/syntaxsaga`):** Missions-Saga in drei **wählbaren Schwierigkeitsstufen**
|
||||
(Junior = eine Lücke, Professional = mehrere Lücken, Senior = ganze Zeile rekonstruieren). Jede
|
||||
Mission besteht aus Erzähltext + optionalem Dialog + Codeblock mit `{{n}}`-Lücken. Eine Lücke kann
|
||||
**mehrere gültige Antworten** haben (z. B. `localhost` / `192.168.0.1`) und optional
|
||||
groß-/kleinschreibungs-unabhängig prüfen (z. B. SQL-Keywords). Eingabe per Freitext; **Prüfung und
|
||||
Score ausschließlich serverseitig** – die Lösungen verlassen nie das Backend (Anti-Cheat wie bei
|
||||
TechTacular/MindMatch).
|
||||
- **Fehler-Mechanik:** pro Lücke bis zu drei Versuche mit Hinweis, danach **„Auflösung anzeigen"**; die
|
||||
Geschichte läuft immer weiter (**kein Game-Over**). Punkte je nach Versuch (Erstversuch voll, danach
|
||||
weniger), aufgelöste Lücke = 0 Punkte; Sternewertung 0–3.
|
||||
- **Admin-Verwaltung (`/admin/syntaxsaga`):** Missionen, Lücken und Lösungen anlegen/bearbeiten/löschen,
|
||||
aktiv/inaktiv schalten. Mitgeliefert: **34 Seed-Missionen** (16 Junior · 10 Professional · 8 Senior,
|
||||
zusammen 44 Lücken) aus `syntaxsaga/content.json` (idempotenter Seeder, lädt nur in leere Tabellen).
|
||||
Themen quer durch den Stack: **Java** (Schleifen, OOP/`class`/`this`, Exceptions, Collections,
|
||||
Operatoren), **SQL** (`WHERE`/`ORDER BY`/`JOIN … ON`/`INSERT`/`UPDATE`/`DELETE`), **Bash**
|
||||
(`cd`/`grep`/Pipe), **Git** (`commit`), **HTML** (Link/Formular) und **Python** (`print`); Senior
|
||||
rekonstruiert ganze Zeilen (z. B. `public static void main(String[] args)`,
|
||||
`Scanner sc = new Scanner(System.in)`) mit ein paar tolerierten Schreibvarianten.
|
||||
- Spielstände laufen über die bestehende generische `game_scores`-Tabelle (Spiel-Kennung `syntax-saga`),
|
||||
inkl. Highscore/„Neuer Bestwert" und Statistik im Spiel-HUD.
|
||||
|
||||
### Technik
|
||||
- Neue Entities `SyntaxSagaMission` / `SyntaxSagaGap` / `SyntaxSagaSolution` + Flyway-Migration
|
||||
`V7__syntax_saga.sql`; server-autoritativer `SyntaxSagaService` (Story ohne Lösungen / serverseitige
|
||||
Prüfung / Score-Recompute beim Abschluss); JSON-API `/api/syntaxsaga/**` (CSRF-ausgenommen). Frontend
|
||||
unter `syntaxsaga.html` / `syntaxsaga.js` / `syntaxsaga.css` (Prefix `ss-`, zentrale `theme.css`-Tokens).
|
||||
|
||||
## [Unreleased] – 2026-06-29
|
||||
|
||||
Inhaltlicher und optischer Feinschliff: echte Rechtstexte, vollständig
|
||||
überarbeitete FinalFlux-Inhalte, neue Login-Optik, eine reparierte
|
||||
Konto-Löschung sowie ein app-weiter Farb-Relaunch auf das
|
||||
„Back to the Future"-Schema.
|
||||
|
||||
### Hinzugefügt
|
||||
- **Rechtsseiten:** Impressum-Platzhalter (Hinweis „in Entwicklung / nicht für die öffentliche Nutzung") sowie **echte** Nutzungsbedingungen/AGB, Datenschutzerklärung (DSGVO-konform, spiegelt die tatsächlich verarbeiteten Daten) und Community-/Nutzungsrichtlinien. Neuer `LegalController` (`/nutzungsbedingungen`, `/datenschutz`, `/richtlinien`) + `ImpressumController`; alle Footer-Links app-weit verdrahtet.
|
||||
- **Kombinierte Login/Registrierung:** Dual-Panel mit überschiebendem Overlay (Sign-in ↔ „Account erstellen") und Neon-Button mit Hover-Expand und rotierendem Conic-Glow; ausgelagert nach `static/css/auth.css`. Responsiver Fallback (gestapelt) auf schmalen Screens.
|
||||
- **FinalFlux – Netzwerktopologien:** fünf theme-adaptive Inline-SVG-Diagramme (Stern, Bus, Ring, Mesh, Baum) in den Erklärungen.
|
||||
|
||||
### Geändert
|
||||
- **FinalFlux-Inhalte komplett überarbeitet (28 Themen, ~742 Fragen):** die generischen Dummy-Falschantworten („nur Dekoration", „zufälliger Begriff" …) durch plausible, themennahe Distraktoren ersetzt, jede Frage mit echter Begründung und variierender Antwortposition. Jede Themenseite hat jetzt **echten Lehrtext** (Definitionen, Funktionsweise, Rechenbeispiele, Merksätze) statt des bisherigen Stubs.
|
||||
- **App-weiter Farb-Relaunch auf „Back to the Future"-Palette:** Primär **Feuer-Amber** (`#f0a81a`) + Gelb-Glow, **Flux-Blau** (`#2d9cce`) als Zweitakzent (u. a. Links); warmer Bernstein-Glow statt Matrix-Grün. Dark/Light bleiben (sehr dunkler/sehr heller Grund). Zentral über `theme.css`-Tokens; Legacy-Cyan in `style.css` auf Theme-Variablen umgestellt.
|
||||
- **Spiele-Feedback ohne Grün:** richtig = **Flux-Blau + ✓**, falsch = **Rot + ✗** in FinalFlux, MindMatch und TechTacular (löst den fehlenden Kontrast von „grün auf grün"); neue Tokens `--success`, `--danger`, `--secondary`. Das Fehler-Rot ist ein kühleres Crimson (`#ff4d6d` dunkel / `#be123c` hell), damit es sich klar vom orangen Akzent abhebt (v. a. im Light-Mode).
|
||||
- Registrierungs-Duplikat leitet auf `/?registerError` (kombinierte Seite öffnet automatisch das Signup-Panel).
|
||||
- **Vollständig responsive / mobiltauglich:** Auf schmalen Screens wird die App-Sidebar nicht mehr ausgeblendet (vorher fehlte die Navigation komplett), sondern zu einer Off-Canvas-Schublade mit Hamburger-Button + Backdrop (global via `theme.js` injiziert, Styles in `theme.css`). Topbar stapelt/umbricht (dekorativer Puls + Suche ausgeblendet), Spielkarten-Grid 3→2→1, breite Tabellen horizontal scrollbar. Spiele, FinalFlux und Login/Registrierung waren bereits responsiv.
|
||||
|
||||
### Behoben
|
||||
- **Konto-Löschung repariert:** „Profil löschen" warf einen 500er (fehlendes `@Transactional` bei einer abgeleiteten Delete-Query) und löschte nichts. Jetzt werden Konto **plus** Spielstände, Achievements und Profilbild-Datei in einer Transaktion entfernt — erfüllt die in der Datenschutzerklärung zugesicherte Löschung (Art. 17 DSGVO).
|
||||
- Impressum-„Zurück"-Link loggte eingeloggte Nutzer scheinbar aus; jetzt login-abhängig (eingeloggt → Dashboard, sonst → Startseite). Gleiche Logik auf den Rechtsseiten.
|
||||
- Login-Overlay im **Light-Mode** war weiß-auf-weiß/verwaschen; jetzt kräftiges Orange-Panel mit weißem Text (Dark-Mode unverändert).
|
||||
- **Mobile-Bedienbarkeit der Spiele:** MindMatch- und TechTacular-Boards ragten auf dem Handy aus dem Bildschirm, ohne dass man scrollen konnte (`height:100vh; overflow:hidden` + iOS-Adressleiste). Auf Touch-Breiten jetzt scrollbar, Karten/Kacheln mit fester Proportion bzw. Mindesthöhe → alle Felder erreichbar.
|
||||
- **FinalFlux-Hub auf Mobil:** Überlappung von Kopfbereich und Inhalt + unerreichbares Themen-Menü. Ursache: das generische `main {}` aus der Alt-`style.css` traf das Hub-`<main>`; jetzt in `.finalflux-main` neutralisiert und die Shell stapelt auf Mobil sauber (`display:block`, Sidebar mit voller Höhe).
|
||||
|
||||
### Sicherheit
|
||||
- Neue Rechtsseiten öffentlich erreichbar (`permitAll` für `/impressum`, `/nutzungsbedingungen`, `/datenschutz`, `/richtlinien`).
|
||||
|
||||
### Hinweise
|
||||
- Footer-Links **Support** und **Feedback** sind weiterhin Platzhalter (`href="#"`).
|
||||
- Die Rechtstexte sind inhaltlich vollständig, aber KI-erstellt — vor einem öffentlichen Launch fachlich/juristisch prüfen (insb. konkrete Anbieteranschrift im Impressum).
|
||||
- Vier ungenutzte Alt-CSS-Dateien (`progress/settings/achievements/soziales.css`, keine Referenzen) blieben vorerst liegen.
|
||||
|
||||
## [Unreleased] – 2026-06-25
|
||||
|
||||
Großer Ausbau: zwei vollständige Spiele, durchgängig DB-gestützte und
|
||||
admin-pflegbare Inhalte sowie ein kompletter Optik-Relaunch (Theme-System
|
||||
mit Dark/Light-Umschaltung).
|
||||
|
||||
### Hinzugefügt
|
||||
|
||||
#### Spiel: MindMatch (Lern-Memory)
|
||||
- Memory-Spiel, das **Begriff ↔ Bedeutung** paart (klassische Aufdeck-Mechanik mit Lernwert).
|
||||
- Deck-Auswahl, Zug-/Zeitzähler, Sternebewertung und „Spiel abbrechen".
|
||||
- **No-Scroll-Layout**: alle Karten einer Runde passen ohne Scrollen auf den Schirm (Brett skaliert die Karten).
|
||||
- Kartenrückseiten tragen das leuchtende Mindflux-Emblem (grün/magenta je Theme).
|
||||
- Inhalte **DB-gestützt und im Admin pflegbar** (`/admin/mindmatch`): Decks und Paare jederzeit erweiterbar.
|
||||
- Mitgelieferte Decks: Netzwerk, Hardware, IT-Sicherheit, SQL-Befehle sowie ein großer Fachinformatiker-Satz (15 Decks insgesamt).
|
||||
|
||||
#### Spiel: TechTacular (Jeopardy-Quiz)
|
||||
- **Echtes Jeopardy-Prinzip (umgekehrt):** angezeigt wird eine Aussage, gesucht ist der Begriff als „Was ist …?".
|
||||
- 5×5-Board (Kategorie × Punktwert 100–500), Antwortprüfung mit Erklärung.
|
||||
- **Server-autoritativ & Anti-Cheat:** das Board enthält nie die Lösung; Prüfung und Wertung passieren ausschließlich serverseitig.
|
||||
- **≥ 60 % neue Fragen pro Runde:** das Board schließt die Fragen der Vorrunde aus.
|
||||
- **Kuratierter Inhalt:** 12 Kategorien über das ganze Informatik-Prüfungsspektrum (Netzwerk, Betriebssysteme, Programmierung, Datenstrukturen & Algorithmen, Datenbanken/SQL, IT-Sicherheit & Krypto, Web, Rechnerarchitektur, Software-Engineering, Theoretische Informatik, Cloud/DevOps, IT-Recht), **384 Hinweise**.
|
||||
- Admin-Verwaltung als Drill-down (`/admin/techtacular`).
|
||||
|
||||
#### Generisches Spiel-Backend
|
||||
- Tabelle `game_scores` + `GameScoreService`/`GameScoreController` mit `POST /api/games/{game}/scores` und `GET /api/games/{game}/stats` für alle Spiele.
|
||||
- `/progress` zeigt echte Spiel-Statistiken (Highscore, Spiele, zuletzt gespielt).
|
||||
|
||||
#### Optik-Relaunch / Theme-System
|
||||
- **Zentrale Tokens** in `theme.css` + Umschalter `theme.js`, gesteuert über `<html data-theme="dark|light">` (Persistenz via `localStorage`, kein Aufblitzen).
|
||||
- **Dark = Grün-Neon, Light = heller Grund + Magenta** (bewusst invertiert).
|
||||
- Wiederverwendbare `mf-`-Komponenten (Shell/Sidebar/Topbar, HUD-Karten, Panels, Buttons, Formulare, Tabellen, Footer …).
|
||||
- **Dashboard** im „Cyber-Grid"-Look neu: linke Sidebar (Nav, Profil, Level/XP, Streak), Mitte mit HUD-Spielkarten, rechte Widget-Spalte (System-Status, Fortschritt, Top-Kategorien, Challenge), Top-Leiste mit Suche/Glocke/Theme-Schalter.
|
||||
- Original-Mindflux-Logo per CSS-Maske theme-abhängig eingefärbt (`/img/mindflux-emblem.png`).
|
||||
- Theme + Umschalter **app-weit** ausgerollt: Login/Registrierung, beide Spiele, Profil/Fortschritt/Achievements/Einstellungen/Soziales/Knowledge, alle Admin-Seiten, FinalFlux-Hub und 28 statische Lernseiten.
|
||||
- **Fußzeile** (Support · Feedback · Nutzungsbedingungen · Richtlinien · Datenschutz · Impressum) als `.mf-footer`-Komponente, einheitlich auf allen Content-Seiten.
|
||||
|
||||
### Geändert
|
||||
- `ProgressService` liefert echte DB-Daten statt der bisherigen Dummy-Spiele.
|
||||
- Admin-Bootstrap-User auf `AliceDarkest` umbenannt (vorher „Alice Darkest" mit Leerzeichen); zusätzlicher Admin `KevCipher` angelegt.
|
||||
|
||||
### Sicherheit
|
||||
- Statische JS-Ressourcen (`/js/**`) wie CSS/Bilder öffentlich (`permitAll`) — kein schützenswerter Inhalt.
|
||||
- Score-Endpunkte gegen eine Allowlist bekannter Spiele abgesichert; Punkte werden serverseitig berechnet (kein vom Client gemeldeter Score).
|
||||
|
||||
### Technisch
|
||||
- **Flyway-Migrationen V2–V6:** `game_scores` (V2), MindMatch-Decks/-Paare (V3) + Fachinformatiker-Inhalte (V4), TechTacular-Schema (V5) + kuratierte Reverse-Inhalte (V6).
|
||||
- Inhalts-Import für TechTacular über einen `CommandLineRunner`-Seeder aus `techtacular/content.json`.
|
||||
- Hinweis (Thymeleaf): Inline-`<script>` mit `[[`/`]]` benötigt `th:inline="none"`, sonst bricht die Antwort ab.
|
||||
|
||||
### Hinweise
|
||||
- Footer-Links sind aktuell Platzhalter (`href="#"`); echte Impressum-/AGB-/Datenschutz-Seiten existieren noch nicht.
|
||||
- Die fünf übrigen Dashboard-Spiele (Syntax Saga, Code Escape Room, Service Clash, Bug Hunter, Netzwerker) sind noch nicht umgesetzt und verweisen auf „coming soon".
|
||||
|
||||
---
|
||||
|
||||
_Frühere Stände (Grundgerüst der Lernplattform: Benutzer/Login, Profil, Achievements, FinalFlux, Admin) wurden vor Einführung dieses Changelogs entwickelt._
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
# Mindflux – Style Guide
|
||||
|
||||
Verbindliche Design-Referenz für **Mindflux**. **Single Source of Truth ist
|
||||
`src/main/resources/static/css/theme.css`** – diese Datei dokumentiert nur, was dort definiert
|
||||
ist. Bei Änderungen immer zuerst `theme.css` anpassen und diese Datei nachziehen.
|
||||
|
||||
> **Grundregel:** Niemals Hex-/RGBA-Werte hart in Templates oder Spiele-CSS schreiben. **Immer**
|
||||
> die Tokens (`var(--accent)`, `var(--surface)` …) verwenden, damit Dark/Light und künftige
|
||||
> Palettenwechsel automatisch greifen.
|
||||
|
||||
Theme-Umschaltung: über das Attribut `<html data-theme="dark|light">` (gesetzt/persistiert von
|
||||
`theme.js`; injiziert auch den mobilen Off-Canvas-Drawer). **Dark ist Default.**
|
||||
|
||||
---
|
||||
|
||||
## Farbkonzept – „Back to the Future"
|
||||
|
||||
- **Primärakzent = Feuer-Amber** (Dark `#f0a81a` / Light `#d65116`) + **Gelb-Glow** (`--accent-2`)
|
||||
für Verläufe und Leuchten.
|
||||
- **Zweitakzent = Flux-Blau** (`#2d9cce`) – für **Links** und das Spiel-Feedback **„richtig"**.
|
||||
- **Warmer Bernstein-Glow** statt Matrix-Grün; dunkler bzw. sehr heller Grund je Theme.
|
||||
- **Kein Grün** im Erfolgs-Feedback: **richtig = Blau ✓**, **falsch = Rot ✗** (Rot ist ein kühles
|
||||
Crimson, damit es sich klar vom orangen Akzent abhebt – v. a. im Light-Mode).
|
||||
|
||||
---
|
||||
|
||||
## Token-Referenz (aus `theme.css`)
|
||||
|
||||
| Token | Dark | Light | Verwendung |
|
||||
|---|---|---|---|
|
||||
| `--bg` | `#0a0806` | `#fbf6ec` | Seitenhintergrund (unter `--bg-glow`) |
|
||||
| `--bg-glow` | radial `rgba(240,168,26,.10)` | radial `rgba(214,81,22,.10)` | weicher Lichtschein oben |
|
||||
| `--panel` | `#13100b` | `#ffffff` | Sidebar, Topbar, Footer |
|
||||
| `--panel-2` | `#171209` | `#fdf7ea` | Panel-Variante |
|
||||
| `--surface` | `#120f0a` | `#ffffff` | Karten / Panels / Eingaben (Grundfläche) |
|
||||
| `--surface-2` | `#1b1610` | `#fbf4e6` | erhöhte Fläche, Hover, Felder |
|
||||
| `--border` | `#2a2117` | `#ece1cf` | Standard-Rahmen |
|
||||
| `--line` | `#221b12` | `#f1e8d8` | feine Linien, Scrollbar, Chart-Achsen |
|
||||
| `--border-accent` | `rgba(240,168,26,.45)` | `rgba(214,81,22,.40)` | akzentuierte Rahmen |
|
||||
| `--accent` | `#f0a81a` | `#d65116` | **Primärakzent**: Buttons, aktive Nav, Icons, Headline-Akzent |
|
||||
| `--accent-2` | `#f7dc11` | `#e4801d` | Sekundär-Akzent für Verläufe (Gelb-Glow) |
|
||||
| `--accent-ink` | `#1c1100` | `#ffffff` | Textfarbe **auf** Akzentflächen |
|
||||
| `--secondary` | `#2d9cce` | `#1f86b8` | **Flux-Blau**: Links + „richtig" |
|
||||
| `--secondary-2` | `#5cc4e8` | `#2d9cce` | helleres Blau (Hover/Variante) |
|
||||
| `--text` | `#ece3d2` | `#33271c` | Fließtext |
|
||||
| `--muted` | `#8c8170` | `#8a7d6c` | gedämpfter Text, Labels, Captions |
|
||||
| `--heading` | `#ffffff` | `#241a12` | Überschriften |
|
||||
| `--success` | `#2d9cce` | `#1f86b8` | **„richtig"** (= Flux-Blau, bewusst kein Grün) |
|
||||
| `--danger` | `#ff4d6d` | `#be123c` | **„falsch"** / Fehler / Löschen (kühles Crimson) |
|
||||
| `--glow-s` | `rgba(240,168,26,.55)` | `rgba(214,81,22,.40)` | **starker** Glow (Text-/Drop-Shadow auf Akzent) |
|
||||
| `--glow-w` | `rgba(240,168,26,.16)` | `rgba(214,81,22,.12)` | **weicher** Ambient-Glow |
|
||||
| `--g-soft` | `0 0 18px var(--glow-w)` | `0 6px 18px var(--glow-w)` | Standard-Panel-Schatten |
|
||||
|
||||
### Semantik kurz
|
||||
- **Akzent (Amber/Orange):** alles Primäre/Interaktive. Text auf Akzent immer `--accent-ink`.
|
||||
- **`--accent` + `--accent-2`** zusammen ergeben die typischen Verläufe (`linear-gradient(…,var(--accent),var(--accent-2))`).
|
||||
- **Blau (`--secondary` / `--success`):** Links und Erfolg.
|
||||
- **Rot (`--danger`):** Fehler, Löschen-Buttons, „falsch".
|
||||
|
||||
---
|
||||
|
||||
## Spiele-Feedback (verbindlich)
|
||||
|
||||
In allen Spielen (FinalFlux, MindMatch, TechTacular, Syntax Saga) gilt einheitlich:
|
||||
|
||||
- **richtig** → `var(--success)` (Blau) + Häkchen `✓`
|
||||
- **falsch** → `var(--danger)` (Rot) + Kreuz `✗`
|
||||
- aufgelöst/neutral (Syntax Saga) → `var(--secondary)`
|
||||
|
||||
Muster (siehe z. B. `syntaxsaga.css` / `techtacular.css`):
|
||||
```css
|
||||
.x-feedback.correct { color: var(--success); }
|
||||
.x-feedback.correct::before { content: "\2713 "; }
|
||||
.x-feedback.wrong { color: var(--danger); }
|
||||
.x-feedback.wrong::before { content: "\2717 "; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Typografie
|
||||
|
||||
- **UI/Fließtext:** `"Inter","Segoe UI",system-ui,-apple-system,Roboto,sans-serif`
|
||||
- **Code/Monospace:** `"JetBrains Mono","Fira Code","Consolas","Courier New",monospace`
|
||||
- **Überschriften:** `font-weight:800`, Farbe `--heading`.
|
||||
- **Eyebrow/Labels (`.mf-eyebrow`, Panel-`h4`):** `--accent`, `text-transform:uppercase`,
|
||||
`letter-spacing:.08–.12em`, kleine Schrift.
|
||||
|
||||
---
|
||||
|
||||
## Glow, Schatten & Radien
|
||||
|
||||
- **Glow stark** `--glow-s`: `text-shadow`/`drop-shadow` auf leuchtenden Akzentelementen.
|
||||
- **Glow weich** `--glow-w`: dezenter Schein um Panels/Buttons.
|
||||
- **Panel-Schatten:** `box-shadow: var(--g-soft)`.
|
||||
- **Radien (Konvention):** Badges/Buttons ~`7–9px`, Inputs/Icon-Buttons `10px`, Nav `11px`,
|
||||
Panels `13–14px`, Standalone-Karten `16–18px`, Pills/Bars `99px`.
|
||||
- **Spielkarten** (`.mf-card`) haben eine **abgeschrägte Ecke** via `clip-path` (`--n:18px`) statt
|
||||
rundem Radius – ein bewusstes „HUD"-Detail.
|
||||
|
||||
---
|
||||
|
||||
## Layout-Shell & Responsivität
|
||||
|
||||
App-Seiten nutzen das Grid `.mf-shell` (definiert in `theme.css`):
|
||||
|
||||
```
|
||||
┌── Sidebar 264px ──┬─────── Topbar (Spalte 2–3) ───────┐
|
||||
│ ├──── Main 1fr ────┬─ Rightbar 332px ┤
|
||||
└───────────────────┴──────────────────┴─────────────────┘
|
||||
```
|
||||
- `.mf-shell.no-right` → nur `264px | 1fr` (ohne rechte Spalte).
|
||||
- **Breakpoints:** `≤1180px` rechte Spalte aus, 2-spaltig · `≤820px` Shell stapelt, Sidebar wird
|
||||
zum **Off-Canvas-Drawer** (Hamburger + Backdrop, via `theme.js`) · `≤560px` Spielkarten-Grid 1-spaltig.
|
||||
- Spielseiten (MindMatch/TechTacular/Syntax Saga) haben eigene, schlankere Shells, nutzen aber
|
||||
dieselben Tokens und sind ebenfalls mobil-tauglich.
|
||||
|
||||
---
|
||||
|
||||
## Komponenten-Cheatsheet (`.mf-*` aus `theme.css`)
|
||||
|
||||
| Klasse | Zweck |
|
||||
|---|---|
|
||||
| `.mf-btn` | Primär-Button (Akzent). Varianten: `.outline`, `.ghost`, `.danger` |
|
||||
| `.mf-panel` | Karte/Box; `.accent` = akzentuierter Rahmen + Glow |
|
||||
| `.mf-card` / `.mf-grid` | HUD-Spielkarte (abgeschrägte Ecke) im 3er-Grid |
|
||||
| `.mf-input`, `.mf-select`, `textarea.mf-input` | Formularfelder (Fokus = Akzentrahmen + Glow) |
|
||||
| `.mf-field`, `.mf-label` | Formular-Zeile + Label |
|
||||
| `.mf-table` | Tabellen (Kopf in Akzent, mobil horizontal scrollbar) |
|
||||
| `.mf-badge` | kleines Status-Label |
|
||||
| `.mf-eyebrow` | Kicker/Overline über Headlines |
|
||||
| `.mf-nav` / `.mf-sidebar` | linke Navigation (aktiv = Akzent-Verlauf) |
|
||||
| `.mf-bar` + `> i` | Fortschrittsbalken |
|
||||
| `.mf-center` / `.mf-cardbox` | zentrierte Standalone-Karte (Login/Rechtsseiten) |
|
||||
| `.mf-container`, `.mf-page-head`, `.mf-back` | einfache Inhaltsseiten + Zurück-Link |
|
||||
| `.mf-footer` | globale Fußzeile |
|
||||
| `.mf-theme` / `.mf-theme-float` | Dark/Light-Umschalter (Logik in `theme.js`) |
|
||||
|
||||
---
|
||||
|
||||
## Pro-Spiel-CSS
|
||||
|
||||
Jedes Spiel hat eine eigene CSS-Datei mit eigenem **Klassen-Prefix**, die **ausschließlich
|
||||
Theme-Tokens** verwendet (keine eigenen Farbwerte):
|
||||
|
||||
| Spiel | Datei | Prefix |
|
||||
|---|---|---|
|
||||
| MindMatch | `mindmatch.css` | `mm-` |
|
||||
| TechTacular | `techtacular.css` | `tt-` |
|
||||
| Syntax Saga | `syntaxsaga.css` | `ss-` |
|
||||
| FinalFlux | `finalflux.css` | `finalflux-` |
|
||||
|
||||
Neues Spiel → neue `<spiel>.css` mit eigenem Prefix anlegen, **nur** `var(--…)`-Tokens nutzen,
|
||||
Feedback-Konvention (Blau/Rot) einhalten.
|
||||
|
||||
---
|
||||
|
||||
## Do / Don't
|
||||
|
||||
**Do**
|
||||
- Farben/Schatten/Glow ausschließlich über Tokens.
|
||||
- Auf Akzentflächen Text in `--accent-ink`.
|
||||
- Erfolg = Blau, Fehler = Rot (nie Grün/Orange für „richtig/falsch").
|
||||
- Neue Komponenten an den `.mf-*`-Bausteinen orientieren.
|
||||
|
||||
**Don't**
|
||||
- Keine hart kodierten Hex-/RGBA-Werte in Templates oder Spiele-CSS.
|
||||
- Nicht die **Legacy-Dateien** als Stil-Referenz nehmen: `achievements.css`, `progress.css`,
|
||||
`settings.css`, `soziales.css` (ungenutzt) sowie alte Schrift-Deklarationen
|
||||
(`'Segoe UI'`/`'Poppins'`) entsprechen **nicht** dem aktuellen System. Maßgeblich ist `theme.css`.
|
||||
|
||||
---
|
||||
|
||||
*Stand: 2026-06-30 · Quelle: `src/main/resources/static/css/theme.css` (BTTF-Palette, Dark/Light).*
|
||||
+20
-3
@@ -5,16 +5,33 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
environment:
|
||||
# --- Datenbank: unverändert wie im bisherigen Deploy ---
|
||||
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/mindfluxdb
|
||||
SPRING_DATASOURCE_USERNAME: mindflux
|
||||
SPRING_DATASOURCE_PASSWORD: mindflux_pass
|
||||
|
||||
# --- App: lauscht intern auf 8080 (nginx -> mindflux:8080) ---
|
||||
SERVER_PORT: "8080"
|
||||
MINDFLUX_PUBLIC_URL: "https://mindflux.sysnaptik.de"
|
||||
MINDFLUX_COOKIE_SECURE: "true"
|
||||
|
||||
# --- Bootstrap-Admins: Passwörter kommen aus Gitea-Secrets (siehe Workflow) ---
|
||||
MINDFLUX_ADMIN_USERNAME: "AliceDarkest"
|
||||
MINDFLUX_ADMIN_EMAIL: "AliceDarkest@gmail.com"
|
||||
MINDFLUX_ADMIN_PASSWORD: "${MINDFLUX_ADMIN_PASSWORD}"
|
||||
MINDFLUX_ADMIN2_USERNAME: "KevCipher"
|
||||
MINDFLUX_ADMIN2_EMAIL: "kevcipher@proton.me"
|
||||
MINDFLUX_ADMIN2_PASSWORD: "${MINDFLUX_ADMIN2_PASSWORD}"
|
||||
|
||||
volumes:
|
||||
- mindflux_uploads:/app/uploads
|
||||
|
||||
networks:
|
||||
- backend_network
|
||||
|
||||
ports:
|
||||
- "8081:8080"
|
||||
|
||||
networks:
|
||||
backend_network:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
mindflux_uploads:
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
hello ci pipeline
|
||||
versuch 2
|
||||
Versuch 3
|
||||
Versuch 4
|
||||
Versuch 5
|
||||
Versuch 6
|
||||
Versuch 7
|
||||
Versuch 8
|
||||
Versuch 9
|
||||
Versuch 10
|
||||
Versuch 11
|
||||
Versuch 12
|
||||
Versuch 13
|
||||
Versuch 14 Claude Fix
|
||||
Versuch 15 Maven Fix
|
||||
Versuch 16 Maven Fix pushed
|
||||
rollback
|
||||
@@ -1,59 +1,112 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<groupId>com.mindflux</groupId>
|
||||
<artifactId>mindflux</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.4.4</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.mindflux</groupId>
|
||||
<artifactId>mindflux</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>mindflux</name>
|
||||
<description>Mindflux – Lernplattform zur Prüfungsvorbereitung</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Thymeleaf für Templates -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Thymeleaf + Spring Security Integration -->
|
||||
<dependency>
|
||||
<groupId>org.thymeleaf.extras</groupId>
|
||||
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JPA + Hibernate -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Security komplett -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Passwort-Verschlüsselung (BCrypt) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-crypto</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- PostgreSQL Datenbanktreiber -->
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Datenbank-Migrationen -->
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-database-postgresql</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Hot Reload für Entwicklung -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Unit Tests -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>false</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.2</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- PostgreSQL -->
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JPA optional aber sinnvoll -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-database-postgresql</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.example.mindflux;
|
||||
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class AppStartCheck implements CommandLineRunner {
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
System.out.println("✅ App läuft. Sicherheitsmodul sollte aktiv sein.");
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,10 +1,11 @@
|
||||
package com.mindflux;
|
||||
package com.example.mindflux;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class MindfluxApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MindfluxApplication.class, args);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.example.mindflux.config;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.repository.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
|
||||
@Configuration
|
||||
public class AdminSetup {
|
||||
|
||||
@Value("${MINDFLUX_ADMIN_USERNAME:admin}")
|
||||
private String adminUsername;
|
||||
|
||||
@Value("${MINDFLUX_ADMIN_EMAIL:admin@mindflux.de}")
|
||||
private String adminEmail;
|
||||
|
||||
@Value("${MINDFLUX_ADMIN_PASSWORD:}")
|
||||
private String adminPassword;
|
||||
|
||||
// Optionaler zweiter Bootstrap-Admin. Wird nur angelegt, wenn Username + Passwort gesetzt sind.
|
||||
@Value("${MINDFLUX_ADMIN2_USERNAME:}")
|
||||
private String admin2Username;
|
||||
|
||||
@Value("${MINDFLUX_ADMIN2_EMAIL:}")
|
||||
private String admin2Email;
|
||||
|
||||
@Value("${MINDFLUX_ADMIN2_PASSWORD:}")
|
||||
private String admin2Password;
|
||||
|
||||
@Bean
|
||||
public CommandLineRunner createAdmin(UserRepository userRepo, BCryptPasswordEncoder passwordEncoder) {
|
||||
return args -> {
|
||||
createAdminIfAbsent(userRepo, passwordEncoder, adminUsername, adminEmail, adminPassword);
|
||||
createAdminIfAbsent(userRepo, passwordEncoder, admin2Username, admin2Email, admin2Password);
|
||||
};
|
||||
}
|
||||
|
||||
private void createAdminIfAbsent(UserRepository userRepo, BCryptPasswordEncoder passwordEncoder,
|
||||
String username, String email, String password) {
|
||||
if (username == null || username.isBlank()) {
|
||||
return; // Admin-Slot nicht konfiguriert -> still überspringen.
|
||||
}
|
||||
if (password == null || password.isBlank()) {
|
||||
System.out.println("Kein Bootstrap-Admin erstellt: Passwort für '" + username + "' ist nicht gesetzt.");
|
||||
return;
|
||||
}
|
||||
if (userRepo.findByUsername(username).isEmpty()) {
|
||||
User admin = new User();
|
||||
admin.setUsername(username);
|
||||
admin.setEmail(email);
|
||||
admin.setPassword(passwordEncoder.encode(password));
|
||||
admin.setRole("ADMIN");
|
||||
userRepo.save(admin);
|
||||
System.out.println("Bootstrap-Admin erstellt: " + username);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package com.example.mindflux.config;
|
||||
|
||||
import com.example.mindflux.model.DevLabGame;
|
||||
import com.example.mindflux.model.DevLabTask;
|
||||
import com.example.mindflux.repository.DevLabGameRepository;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Befüllt das Dev Lab einmalig (wenn leer) aus devlab/content.json. Jede Aufgabe wird mit
|
||||
* ihrem Typ + vollständigem JSON-Payload gespeichert; der Service liest daraus.
|
||||
*/
|
||||
@Component
|
||||
public class DevLabSeeder implements CommandLineRunner {
|
||||
|
||||
private static final int MAX_PAYLOAD = 8000;
|
||||
|
||||
private final DevLabGameRepository gameRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public DevLabSeeder(DevLabGameRepository gameRepository, ObjectMapper objectMapper) {
|
||||
this.gameRepository = gameRepository;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void run(String... args) {
|
||||
if (gameRepository.count() > 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(new ClassPathResource("devlab/content.json").getInputStream());
|
||||
JsonNode games = root.get("games");
|
||||
if (games == null || !games.isArray()) {
|
||||
System.out.println("⚠️ Dev-Lab-Seed: content.json ohne 'games'.");
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> used = new HashSet<>();
|
||||
int order = 1;
|
||||
int gameCount = 0;
|
||||
int taskCount = 0;
|
||||
int skipped = 0;
|
||||
|
||||
for (JsonNode gNode : games) {
|
||||
String title = gNode.path("title").asText("");
|
||||
JsonNode tasks = gNode.get("tasks");
|
||||
if (title.isBlank() || tasks == null || !tasks.isArray()) {
|
||||
continue;
|
||||
}
|
||||
String base = gNode.hasNonNull("slug") ? gNode.get("slug").asText() : title;
|
||||
DevLabGame game = new DevLabGame(
|
||||
uniqueSlug(base, used),
|
||||
trim(title, 200),
|
||||
trim(textOrNull(gNode, "language"), 50),
|
||||
trim(textOrNull(gNode, "path"), 50),
|
||||
trim(textOrNull(gNode, "description"), 1000),
|
||||
order, true);
|
||||
|
||||
int sort = 0;
|
||||
for (JsonNode t : tasks) {
|
||||
String type = t.path("type").asText("");
|
||||
String prompt = t.path("prompt").asText("");
|
||||
if (type.isBlank() || prompt.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
String payload = t.toString();
|
||||
if (payload.length() > MAX_PAYLOAD) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
game.addTask(new DevLabTask(type, trim(prompt, 300), t.path("points").asInt(100), sort++, payload));
|
||||
taskCount++;
|
||||
}
|
||||
|
||||
if (!game.getTasks().isEmpty()) {
|
||||
gameRepository.save(game);
|
||||
order++;
|
||||
gameCount++;
|
||||
}
|
||||
}
|
||||
System.out.println("✅ Dev Lab: " + gameCount + " Spiele / " + taskCount + " Aufgaben geladen"
|
||||
+ (skipped > 0 ? " (" + skipped + " zu groß, übersprungen)" : "") + ".");
|
||||
} catch (Exception e) {
|
||||
System.out.println("⚠️ Dev-Lab-Seed übersprungen: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String uniqueSlug(String base, Set<String> used) {
|
||||
String slug = base.toLowerCase()
|
||||
.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
|
||||
.replaceAll("[^a-z0-9]+", "-")
|
||||
.replaceAll("(^-|-$)", "");
|
||||
if (slug.isBlank()) {
|
||||
slug = "game";
|
||||
}
|
||||
String candidate = slug;
|
||||
int counter = 2;
|
||||
while (used.contains(candidate) || gameRepository.existsBySlug(candidate)) {
|
||||
candidate = slug + "-" + counter++;
|
||||
}
|
||||
used.add(candidate);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private String textOrNull(JsonNode node, String field) {
|
||||
if (!node.hasNonNull(field)) {
|
||||
return null;
|
||||
}
|
||||
String value = node.get(field).asText("");
|
||||
return value.isBlank() ? null : value;
|
||||
}
|
||||
|
||||
private String trim(String value, int max) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return value.length() <= max ? value : value.substring(0, max);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.example.mindflux.config;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.repository.UserRepository;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.core.userdetails.*;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/", "/login", "/register", "/impressum", "/nutzungsbedingungen", "/datenschutz", "/richtlinien", "/css/**", "/js/**", "/img/**").permitAll()
|
||||
.requestMatchers("/admin/**").hasRole("ADMIN") // NEU: Adminbereich absichern
|
||||
.requestMatchers("/api/users/**").hasRole("ADMIN")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(form -> form
|
||||
.loginPage("/") // login.html liegt auf "/"
|
||||
.loginProcessingUrl("/login")
|
||||
.defaultSuccessUrl("/dashboard", true)
|
||||
.failureUrl("/?error") // zeigt Loginfehler an
|
||||
.permitAll()
|
||||
)
|
||||
.logout(logout -> logout
|
||||
// Logout-Links in den Templates sind GET -> GET-Logout erlauben, sonst 404/Fehler unter CSRF
|
||||
.logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
|
||||
.logoutSuccessUrl("/?logout")
|
||||
.permitAll()
|
||||
)
|
||||
.csrf(csrf -> csrf
|
||||
// Spiele-APIs werden per fetch (application/json) ohne Token aufgerufen -> ausnehmen.
|
||||
// Reine JSON-Endpunkte sind nicht über klassische HTML-Formulare CSRF-ausnutzbar.
|
||||
.ignoringRequestMatchers("/api/games/**", "/api/techtacular/**", "/api/mindmatch/**", "/api/syntaxsaga/**", "/api/devlab/**")
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService(UserRepository userRepo) {
|
||||
return username -> {
|
||||
User user = userRepo.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("Benutzer nicht gefunden"));
|
||||
|
||||
return org.springframework.security.core.userdetails.User
|
||||
.withUsername(user.getUsername())
|
||||
.password(user.getPassword())
|
||||
.roles(user.getRole()) // Achtung: Nur ein String, z.B. "USER" oder "ADMIN"
|
||||
.build();
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BCryptPasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
||||
return config.getAuthenticationManager();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.example.mindflux.config;
|
||||
|
||||
import com.example.mindflux.model.SyntaxSagaGap;
|
||||
import com.example.mindflux.model.SyntaxSagaMission;
|
||||
import com.example.mindflux.model.SyntaxSagaSolution;
|
||||
import com.example.mindflux.repository.SyntaxSagaMissionRepository;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Befüllt Syntax Saga einmalig (wenn leer) aus der kuratierten Inhaltsdatei
|
||||
* syntaxsaga/content.json. Format: Missionen mit Story, Codetemplate (Lücken als
|
||||
* {@code {{n}}}) und je Lücke einer Liste gültiger Lösungen.
|
||||
*/
|
||||
@Component
|
||||
public class SyntaxSagaSeeder implements CommandLineRunner {
|
||||
|
||||
private static final Set<String> DIFFICULTIES = Set.of("junior", "professional", "senior");
|
||||
|
||||
private final SyntaxSagaMissionRepository missionRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public SyntaxSagaSeeder(SyntaxSagaMissionRepository missionRepository, ObjectMapper objectMapper) {
|
||||
this.missionRepository = missionRepository;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void run(String... args) {
|
||||
if (missionRepository.count() > 0) {
|
||||
return; // schon befüllt
|
||||
}
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(
|
||||
new ClassPathResource("syntaxsaga/content.json").getInputStream());
|
||||
JsonNode missions = root.get("missions");
|
||||
if (missions == null || !missions.isArray()) {
|
||||
System.out.println("⚠️ Syntax-Saga-Seed: content.json ohne 'missions'.");
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> usedSlugs = new HashSet<>();
|
||||
int order = 1;
|
||||
int missionCount = 0;
|
||||
int gapCount = 0;
|
||||
|
||||
for (JsonNode mNode : missions) {
|
||||
String title = mNode.path("title").asText("");
|
||||
String code = mNode.path("code").asText("");
|
||||
JsonNode gaps = mNode.get("gaps");
|
||||
if (title.isBlank() || code.isBlank() || gaps == null || !gaps.isArray()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SyntaxSagaMission mission = new SyntaxSagaMission(
|
||||
uniqueSlug(title, usedSlugs),
|
||||
trim(title, 200),
|
||||
normalizeDifficulty(mNode.path("difficulty").asText("junior")),
|
||||
trim(mNode.path("narrative").asText(""), 2000),
|
||||
trim(textOrNull(mNode, "dialog"), 1000),
|
||||
trim(textOrNull(mNode, "language"), 30),
|
||||
trim(code, 2000),
|
||||
trim(textOrNull(mNode, "successText"), 2000),
|
||||
order, true);
|
||||
|
||||
int pos = 1;
|
||||
for (JsonNode gNode : gaps) {
|
||||
JsonNode solutions = gNode.get("solutions");
|
||||
if (solutions == null || !solutions.isArray() || solutions.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
SyntaxSagaGap gap = new SyntaxSagaGap(
|
||||
gNode.path("position").asInt(pos),
|
||||
gNode.path("points").asInt(100),
|
||||
gNode.path("caseSensitive").asBoolean(true),
|
||||
trim(textOrNull(gNode, "hint"), 500),
|
||||
trim(textOrNull(gNode, "placeholder"), 120));
|
||||
|
||||
int sort = 0;
|
||||
for (JsonNode s : solutions) {
|
||||
String st = s.asText("");
|
||||
if (!st.isBlank()) {
|
||||
gap.addSolution(new SyntaxSagaSolution(trim(st, 500), sort++));
|
||||
}
|
||||
}
|
||||
if (!gap.getSolutions().isEmpty()) {
|
||||
mission.addGap(gap);
|
||||
gapCount++;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (!mission.getGaps().isEmpty()) {
|
||||
missionRepository.save(mission);
|
||||
order++;
|
||||
missionCount++;
|
||||
}
|
||||
}
|
||||
System.out.println("✅ Syntax Saga: " + missionCount + " Missionen / " + gapCount + " Lücken geladen.");
|
||||
} catch (Exception e) {
|
||||
System.out.println("⚠️ Syntax-Saga-Seed übersprungen: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeDifficulty(String difficulty) {
|
||||
if (difficulty == null) {
|
||||
return "junior";
|
||||
}
|
||||
String x = difficulty.trim().toLowerCase();
|
||||
return DIFFICULTIES.contains(x) ? x : "junior";
|
||||
}
|
||||
|
||||
private String textOrNull(JsonNode node, String field) {
|
||||
if (!node.hasNonNull(field)) {
|
||||
return null;
|
||||
}
|
||||
String value = node.get(field).asText("");
|
||||
return value.isBlank() ? null : value;
|
||||
}
|
||||
|
||||
private String uniqueSlug(String title, Set<String> used) {
|
||||
String base = title.toLowerCase()
|
||||
.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
|
||||
.replaceAll("[^a-z0-9]+", "-")
|
||||
.replaceAll("(^-|-$)", "");
|
||||
if (base.isBlank()) {
|
||||
base = "mission";
|
||||
}
|
||||
String slug = base;
|
||||
int counter = 2;
|
||||
while (used.contains(slug) || missionRepository.existsBySlug(slug)) {
|
||||
slug = base + "-" + counter++;
|
||||
}
|
||||
used.add(slug);
|
||||
return slug;
|
||||
}
|
||||
|
||||
private String trim(String value, int max) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return value.length() <= max ? value : value.substring(0, max);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.example.mindflux.config;
|
||||
|
||||
import com.example.mindflux.model.TechtacularAnswer;
|
||||
import com.example.mindflux.model.TechtacularCategory;
|
||||
import com.example.mindflux.model.TechtacularQuestion;
|
||||
import com.example.mindflux.repository.TechtacularCategoryRepository;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Befüllt TechTacular einmalig (wenn leer) aus der kuratierten Inhaltsdatei
|
||||
* techtacular/content.json. Format: Jeopardy umgekehrt – pro Hinweis eine Aussage
|
||||
* ("clue") und der gesuchte Begriff ("solution", = richtige Antwort) plus Distraktoren.
|
||||
*/
|
||||
@Component
|
||||
public class TechtacularSeeder implements CommandLineRunner {
|
||||
|
||||
private final TechtacularCategoryRepository categoryRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public TechtacularSeeder(TechtacularCategoryRepository categoryRepository, ObjectMapper objectMapper) {
|
||||
this.categoryRepository = categoryRepository;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void run(String... args) {
|
||||
if (categoryRepository.count() > 0) {
|
||||
return; // schon befüllt
|
||||
}
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(new ClassPathResource("techtacular/content.json").getInputStream());
|
||||
JsonNode categories = root.get("categories");
|
||||
if (categories == null || !categories.isArray()) {
|
||||
System.out.println("⚠️ TechTacular-Seed: content.json ohne 'categories'.");
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> usedSlugs = new HashSet<>();
|
||||
int order = 1;
|
||||
int catCount = 0;
|
||||
int clueCount = 0;
|
||||
|
||||
for (JsonNode catNode : categories) {
|
||||
String title = catNode.path("title").asText("");
|
||||
JsonNode clues = catNode.get("clues");
|
||||
if (title.isBlank() || clues == null || !clues.isArray()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TechtacularCategory category =
|
||||
new TechtacularCategory(uniqueSlug(title, usedSlugs), trim(title, 200), order, true);
|
||||
|
||||
for (JsonNode clue : clues) {
|
||||
String clueText = clue.path("clue").asText("");
|
||||
String solution = clue.path("solution").asText("");
|
||||
JsonNode distractors = clue.get("distractors");
|
||||
if (clueText.isBlank() || solution.isBlank() || distractors == null || !distractors.isArray()) {
|
||||
continue;
|
||||
}
|
||||
int points = clue.path("points").asInt(100);
|
||||
String explanation = clue.hasNonNull("explanation") ? clue.get("explanation").asText() : null;
|
||||
|
||||
TechtacularQuestion question = new TechtacularQuestion(
|
||||
trim(clueText, 1000),
|
||||
(explanation == null || explanation.isBlank()) ? null : trim(explanation, 1000),
|
||||
points);
|
||||
|
||||
int sort = 0;
|
||||
question.addAnswer(new TechtacularAnswer(trim(solution, 500), true, sort++)); // richtige Antwort
|
||||
for (JsonNode d : distractors) {
|
||||
String dt = d.asText("");
|
||||
if (!dt.isBlank()) {
|
||||
question.addAnswer(new TechtacularAnswer(trim(dt, 500), false, sort++));
|
||||
}
|
||||
}
|
||||
|
||||
if (question.getAnswers().size() >= 2) {
|
||||
category.addQuestion(question);
|
||||
clueCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!category.getQuestions().isEmpty()) {
|
||||
categoryRepository.save(category);
|
||||
order++;
|
||||
catCount++;
|
||||
}
|
||||
}
|
||||
System.out.println("✅ TechTacular: " + catCount + " Kategorien / " + clueCount + " Hinweise geladen.");
|
||||
} catch (Exception e) {
|
||||
System.out.println("⚠️ TechTacular-Seed übersprungen: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String uniqueSlug(String title, Set<String> used) {
|
||||
String base = title.toLowerCase()
|
||||
.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
|
||||
.replaceAll("[^a-z0-9]+", "-")
|
||||
.replaceAll("(^-|-$)", "");
|
||||
if (base.isBlank()) {
|
||||
base = "kategorie";
|
||||
}
|
||||
String slug = base;
|
||||
int counter = 2;
|
||||
while (used.contains(slug) || categoryRepository.existsBySlug(slug)) {
|
||||
slug = base + "-" + counter++;
|
||||
}
|
||||
used.add(slug);
|
||||
return slug;
|
||||
}
|
||||
|
||||
private String trim(String value, int max) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return value.length() <= max ? value : value.substring(0, max);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.example.mindflux.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry
|
||||
.addResourceHandler("/uploads/**")
|
||||
.addResourceLocations("file:" + System.getProperty("user.dir") + "/uploads/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.Achievement;
|
||||
import com.example.mindflux.service.AchievementService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
public class AchievementController {
|
||||
|
||||
@Autowired
|
||||
private AchievementService achievementService;
|
||||
|
||||
@GetMapping("/profile/achievements")
|
||||
public String showAchievements(Model model, Principal principal) {
|
||||
String username = principal.getName();
|
||||
List<Achievement> achievements = achievementService.getAchievementsForUser(username);
|
||||
|
||||
// 🔥 Dummy-Daten nur für Tests!
|
||||
if (achievements == null || achievements.isEmpty()) {
|
||||
achievements = List.of(
|
||||
new Achievement("Hangman Hero", "Gewinne 10 Runden Hangman", "hangman.png", true),
|
||||
new Achievement("Memory Meister", "Erreiche 1000 Punkte im Memory", "memory.png", false),
|
||||
new Achievement("Quizkönig", "Beantworte 50 Fragen richtig", "quiz.png", true),
|
||||
new Achievement("Early Bird", "Lerne vor 7 Uhr morgens", "earlybird.png", false)
|
||||
);
|
||||
}
|
||||
|
||||
model.addAttribute("achievements", achievements);
|
||||
|
||||
return "achievements"; // lädt achievements.html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.DevLabGame;
|
||||
import com.example.mindflux.service.DevLabService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/** Admin-Verwaltung der Dev-Lab-Spiele und -Aufgaben (ROLE_ADMIN via /admin/**). */
|
||||
@Controller
|
||||
@RequestMapping("/admin/devlab")
|
||||
public class AdminDevLabController {
|
||||
|
||||
private final DevLabService devLabService;
|
||||
|
||||
public AdminDevLabController(DevLabService devLabService) {
|
||||
this.devLabService = devLabService;
|
||||
}
|
||||
|
||||
@GetMapping("")
|
||||
public String manage(Model model) {
|
||||
model.addAttribute("games", devLabService.gameRows());
|
||||
return "admin/devlab";
|
||||
}
|
||||
|
||||
@GetMapping("/games/{gameId}")
|
||||
public String game(@PathVariable Long gameId, Model model) {
|
||||
DevLabGame game = devLabService.gameWithTasks(gameId);
|
||||
if (game == null) {
|
||||
return "redirect:/admin/devlab";
|
||||
}
|
||||
model.addAttribute("game", game);
|
||||
return "admin/devlab-game";
|
||||
}
|
||||
|
||||
@PostMapping("/games")
|
||||
public String createGame(@RequestParam String title,
|
||||
@RequestParam(required = false) String language,
|
||||
@RequestParam(required = false) String path,
|
||||
@RequestParam(required = false) String description) {
|
||||
Long id = devLabService.createGame(title, language, path, description);
|
||||
return id == null ? "redirect:/admin/devlab" : "redirect:/admin/devlab/games/" + id;
|
||||
}
|
||||
|
||||
@PostMapping("/games/{gameId}/tasks")
|
||||
public String addTask(@PathVariable Long gameId,
|
||||
@RequestParam String type,
|
||||
@RequestParam String prompt,
|
||||
@RequestParam(defaultValue = "100") int points,
|
||||
@RequestParam String payload) {
|
||||
devLabService.addTask(gameId, type, prompt, points, payload);
|
||||
return "redirect:/admin/devlab/games/" + gameId;
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/update")
|
||||
public String updateTask(@PathVariable Long taskId,
|
||||
@RequestParam Long gameId,
|
||||
@RequestParam String prompt,
|
||||
@RequestParam(defaultValue = "100") int points,
|
||||
@RequestParam String payload) {
|
||||
devLabService.updateTask(taskId, prompt, points, payload);
|
||||
return "redirect:/admin/devlab/games/" + gameId;
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/delete")
|
||||
public String deleteTask(@PathVariable Long taskId, @RequestParam Long gameId) {
|
||||
devLabService.deleteTask(taskId);
|
||||
return "redirect:/admin/devlab/games/" + gameId;
|
||||
}
|
||||
|
||||
@PostMapping("/games/{gameId}/delete")
|
||||
public String deleteGame(@PathVariable Long gameId) {
|
||||
devLabService.deleteGame(gameId);
|
||||
return "redirect:/admin/devlab";
|
||||
}
|
||||
|
||||
@PostMapping("/games/{gameId}/toggle")
|
||||
public String toggleGame(@PathVariable Long gameId) {
|
||||
devLabService.toggleGame(gameId);
|
||||
return "redirect:/admin/devlab";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.service.MindMatchService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* Admin-Verwaltung der MindMatch-Decks und -Paare. Liegt unter /admin/** und ist
|
||||
* damit per SecurityConfig auf ROLE_ADMIN beschränkt.
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/admin/mindmatch")
|
||||
public class AdminMindMatchController {
|
||||
|
||||
private final MindMatchService mindMatchService;
|
||||
|
||||
public AdminMindMatchController(MindMatchService mindMatchService) {
|
||||
this.mindMatchService = mindMatchService;
|
||||
}
|
||||
|
||||
@GetMapping("")
|
||||
public String manage(Model model) {
|
||||
model.addAttribute("decks", mindMatchService.allDecksWithPairs());
|
||||
return "admin/mindmatch";
|
||||
}
|
||||
|
||||
@PostMapping("/decks")
|
||||
public String createDeck(@RequestParam String title) {
|
||||
mindMatchService.createDeck(title);
|
||||
return "redirect:/admin/mindmatch";
|
||||
}
|
||||
|
||||
@PostMapping("/decks/{deckId}/pairs")
|
||||
public String addPair(@PathVariable Long deckId,
|
||||
@RequestParam String term,
|
||||
@RequestParam String match) {
|
||||
mindMatchService.addPair(deckId, term, match);
|
||||
return "redirect:/admin/mindmatch";
|
||||
}
|
||||
|
||||
@PostMapping("/pairs/{pairId}/delete")
|
||||
public String deletePair(@PathVariable Long pairId) {
|
||||
mindMatchService.deletePair(pairId);
|
||||
return "redirect:/admin/mindmatch";
|
||||
}
|
||||
|
||||
@PostMapping("/decks/{deckId}/delete")
|
||||
public String deleteDeck(@PathVariable Long deckId) {
|
||||
mindMatchService.deleteDeck(deckId);
|
||||
return "redirect:/admin/mindmatch";
|
||||
}
|
||||
|
||||
@PostMapping("/decks/{deckId}/toggle")
|
||||
public String toggleDeck(@PathVariable Long deckId) {
|
||||
mindMatchService.toggleDeck(deckId);
|
||||
return "redirect:/admin/mindmatch";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.SyntaxSagaMission;
|
||||
import com.example.mindflux.service.SyntaxSagaService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/** Admin-Verwaltung der Syntax-Saga-Missionen, -Lücken und -Lösungen (ROLE_ADMIN via /admin/**). */
|
||||
@Controller
|
||||
@RequestMapping("/admin/syntaxsaga")
|
||||
public class AdminSyntaxSagaController {
|
||||
|
||||
private final SyntaxSagaService syntaxSagaService;
|
||||
|
||||
public AdminSyntaxSagaController(SyntaxSagaService syntaxSagaService) {
|
||||
this.syntaxSagaService = syntaxSagaService;
|
||||
}
|
||||
|
||||
/** Übersicht aller Missionen. */
|
||||
@GetMapping("")
|
||||
public String manage(Model model) {
|
||||
model.addAttribute("missions", syntaxSagaService.missionRows());
|
||||
return "admin/syntaxsaga";
|
||||
}
|
||||
|
||||
/** Detailseite einer Mission mit ihren Lücken. */
|
||||
@GetMapping("/missions/{missionId}")
|
||||
public String mission(@PathVariable Long missionId, Model model) {
|
||||
SyntaxSagaMission mission = syntaxSagaService.missionWithGaps(missionId);
|
||||
if (mission == null) {
|
||||
return "redirect:/admin/syntaxsaga";
|
||||
}
|
||||
model.addAttribute("mission", mission);
|
||||
return "admin/syntaxsaga-mission";
|
||||
}
|
||||
|
||||
@PostMapping("/missions")
|
||||
public String createMission(@RequestParam String title,
|
||||
@RequestParam(required = false) String difficulty,
|
||||
@RequestParam(required = false) String narrative,
|
||||
@RequestParam(required = false) String dialog,
|
||||
@RequestParam(required = false) String language,
|
||||
@RequestParam String codeTemplate,
|
||||
@RequestParam(required = false) String successText) {
|
||||
Long id = syntaxSagaService.createMission(title, difficulty, narrative, dialog,
|
||||
language, codeTemplate, successText);
|
||||
return id == null ? "redirect:/admin/syntaxsaga"
|
||||
: "redirect:/admin/syntaxsaga/missions/" + id;
|
||||
}
|
||||
|
||||
@PostMapping("/missions/{missionId}/update")
|
||||
public String updateMission(@PathVariable Long missionId,
|
||||
@RequestParam String title,
|
||||
@RequestParam(required = false) String difficulty,
|
||||
@RequestParam(required = false) String narrative,
|
||||
@RequestParam(required = false) String dialog,
|
||||
@RequestParam(required = false) String language,
|
||||
@RequestParam String codeTemplate,
|
||||
@RequestParam(required = false) String successText) {
|
||||
syntaxSagaService.updateMission(missionId, title, difficulty, narrative, dialog,
|
||||
language, codeTemplate, successText);
|
||||
return "redirect:/admin/syntaxsaga/missions/" + missionId;
|
||||
}
|
||||
|
||||
@PostMapping("/missions/{missionId}/gaps")
|
||||
public String addGap(@PathVariable Long missionId,
|
||||
@RequestParam(defaultValue = "0") int position,
|
||||
@RequestParam(defaultValue = "100") int points,
|
||||
@RequestParam(defaultValue = "false") boolean caseSensitive,
|
||||
@RequestParam(required = false) String hint,
|
||||
@RequestParam(required = false) String placeholder,
|
||||
@RequestParam(required = false, defaultValue = "") String solutions) {
|
||||
List<String> parsed = Arrays.stream(solutions.split("\\r?\\n"))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.toList();
|
||||
syntaxSagaService.addGap(missionId, position, points, caseSensitive, hint, placeholder, parsed);
|
||||
return "redirect:/admin/syntaxsaga/missions/" + missionId;
|
||||
}
|
||||
|
||||
@PostMapping("/gaps/{gapId}/delete")
|
||||
public String deleteGap(@PathVariable Long gapId, @RequestParam Long missionId) {
|
||||
syntaxSagaService.deleteGap(gapId);
|
||||
return "redirect:/admin/syntaxsaga/missions/" + missionId;
|
||||
}
|
||||
|
||||
@PostMapping("/missions/{missionId}/delete")
|
||||
public String deleteMission(@PathVariable Long missionId) {
|
||||
syntaxSagaService.deleteMission(missionId);
|
||||
return "redirect:/admin/syntaxsaga";
|
||||
}
|
||||
|
||||
@PostMapping("/missions/{missionId}/toggle")
|
||||
public String toggleMission(@PathVariable Long missionId) {
|
||||
syntaxSagaService.toggleMission(missionId);
|
||||
return "redirect:/admin/syntaxsaga";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.TechtacularCategory;
|
||||
import com.example.mindflux.service.TechtacularService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/** Admin-Verwaltung der TechTacular-Kategorien, -Fragen und -Antworten (ROLE_ADMIN via /admin/**). */
|
||||
@Controller
|
||||
@RequestMapping("/admin/techtacular")
|
||||
public class AdminTechtacularController {
|
||||
|
||||
private final TechtacularService techtacularService;
|
||||
|
||||
public AdminTechtacularController(TechtacularService techtacularService) {
|
||||
this.techtacularService = techtacularService;
|
||||
}
|
||||
|
||||
/** Übersicht aller Kategorien. */
|
||||
@GetMapping("")
|
||||
public String manage(Model model) {
|
||||
model.addAttribute("categories", techtacularService.categoryRows());
|
||||
return "admin/techtacular";
|
||||
}
|
||||
|
||||
/** Detailseite einer Kategorie mit ihren Fragen. */
|
||||
@GetMapping("/categories/{categoryId}")
|
||||
public String category(@PathVariable Long categoryId, Model model) {
|
||||
TechtacularCategory category = techtacularService.categoryWithQuestions(categoryId);
|
||||
if (category == null) {
|
||||
return "redirect:/admin/techtacular";
|
||||
}
|
||||
model.addAttribute("category", category);
|
||||
return "admin/techtacular-category";
|
||||
}
|
||||
|
||||
@PostMapping("/categories")
|
||||
public String createCategory(@RequestParam String title) {
|
||||
techtacularService.createCategory(title);
|
||||
return "redirect:/admin/techtacular";
|
||||
}
|
||||
|
||||
@PostMapping("/categories/{categoryId}/questions")
|
||||
public String addQuestion(@PathVariable Long categoryId,
|
||||
@RequestParam String text,
|
||||
@RequestParam(required = false) String explanation,
|
||||
@RequestParam(defaultValue = "100") int points,
|
||||
@RequestParam(required = false, defaultValue = "") String answer1,
|
||||
@RequestParam(required = false, defaultValue = "") String answer2,
|
||||
@RequestParam(required = false, defaultValue = "") String answer3,
|
||||
@RequestParam(required = false, defaultValue = "") String answer4,
|
||||
@RequestParam(defaultValue = "0") int correct) {
|
||||
List<String> answers = Arrays.asList(answer1, answer2, answer3, answer4);
|
||||
techtacularService.addQuestion(categoryId, text, explanation, points, answers, correct);
|
||||
return "redirect:/admin/techtacular/categories/" + categoryId;
|
||||
}
|
||||
|
||||
@PostMapping("/questions/{questionId}/delete")
|
||||
public String deleteQuestion(@PathVariable Long questionId, @RequestParam Long categoryId) {
|
||||
techtacularService.deleteQuestion(questionId);
|
||||
return "redirect:/admin/techtacular/categories/" + categoryId;
|
||||
}
|
||||
|
||||
@PostMapping("/categories/{categoryId}/delete")
|
||||
public String deleteCategory(@PathVariable Long categoryId) {
|
||||
techtacularService.deleteCategory(categoryId);
|
||||
return "redirect:/admin/techtacular";
|
||||
}
|
||||
|
||||
@PostMapping("/categories/{categoryId}/toggle")
|
||||
public String toggleCategory(@PathVariable Long categoryId) {
|
||||
techtacularService.toggleCategory(categoryId);
|
||||
return "redirect:/admin/techtacular";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.service.AdminUserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/admin")
|
||||
public class AdminUserController {
|
||||
|
||||
@Autowired
|
||||
private AdminUserService userService;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
// ⬅️ Neu: Dashboard-Startseite
|
||||
@GetMapping("")
|
||||
public String adminDashboard() {
|
||||
return "admin/dashboard"; // du brauchst: templates/admin/dashboard.html
|
||||
}
|
||||
|
||||
// ⬅️ Benutzerverwaltung
|
||||
@GetMapping("/users")
|
||||
public String listUsers(Model model) {
|
||||
model.addAttribute("users", userService.findAll());
|
||||
return "admin/users/list";
|
||||
}
|
||||
|
||||
@GetMapping("/users/new")
|
||||
public String createUserForm(Model model) {
|
||||
model.addAttribute("user", new User());
|
||||
return "admin/users/form";
|
||||
}
|
||||
|
||||
@PostMapping("/users/save")
|
||||
public String saveUser(@ModelAttribute("user") User user) {
|
||||
if (user.getId() != null && (user.getPassword() == null || user.getPassword().isBlank())) {
|
||||
User existingUser = userService.findById(user.getId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Ungültige User-ID: " + user.getId()));
|
||||
user.setPassword(existingUser.getPassword());
|
||||
} else {
|
||||
user.setPassword(passwordEncoder.encode(user.getPassword()));
|
||||
}
|
||||
|
||||
userService.save(user);
|
||||
return "redirect:/admin/users";
|
||||
}
|
||||
|
||||
@GetMapping("/users/edit/{id}")
|
||||
public String editUserForm(@PathVariable Long id, Model model) {
|
||||
User user = userService.findById(id).orElseThrow(() -> new IllegalArgumentException("Ungültige User-ID: " + id));
|
||||
model.addAttribute("user", user);
|
||||
return "admin/users/form";
|
||||
}
|
||||
|
||||
@PostMapping("/users/delete/{id}")
|
||||
public String deleteUser(@PathVariable Long id) {
|
||||
userService.deleteById(id);
|
||||
return "redirect:/admin/users";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class DashboardController {
|
||||
|
||||
@GetMapping("/dashboard")
|
||||
public String showDashboard() {
|
||||
return "dashboard"; // lädt dashboard.html aus templates
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.dto.DevLabCheckRequest;
|
||||
import com.example.mindflux.dto.DevLabFinishRequest;
|
||||
import com.example.mindflux.dto.DevLabResult;
|
||||
import com.example.mindflux.service.DevLabService;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
/** JSON-API fürs Dev Lab. Server-autoritativ: Lösungen werden nie ausgeliefert. */
|
||||
@RestController
|
||||
@RequestMapping("/api/devlab")
|
||||
public class DevLabApiController {
|
||||
|
||||
private final DevLabService devLabService;
|
||||
|
||||
public DevLabApiController(DevLabService devLabService) {
|
||||
this.devLabService = devLabService;
|
||||
}
|
||||
|
||||
/** Spiele-Katalog für den Hub (nach Pfad gruppierbar). */
|
||||
@GetMapping("/catalog")
|
||||
public JsonNode catalog() {
|
||||
return devLabService.buildCatalog();
|
||||
}
|
||||
|
||||
/** Eine Spielrunde: Aufgaben einer Disziplin – ohne Lösungen. */
|
||||
@GetMapping("/games/{slug}/session")
|
||||
public JsonNode session(@PathVariable String slug) {
|
||||
return devLabService.buildSession(slug);
|
||||
}
|
||||
|
||||
/** Eine Aufgabe serverseitig prüfen; mit {@code reveal=true} auch die Auflösung. */
|
||||
@PostMapping("/check")
|
||||
public JsonNode check(@RequestBody DevLabCheckRequest request) {
|
||||
return devLabService.check(request.getTaskId(), request.isReveal(), request.getSubmission());
|
||||
}
|
||||
|
||||
/** Durchlauf abschließen: Score wird serverseitig neu berechnet und gespeichert. */
|
||||
@PostMapping("/finish")
|
||||
public DevLabResult finish(@RequestBody DevLabFinishRequest request, Principal principal) {
|
||||
return devLabService.finish(principal.getName(), request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
|
||||
@Controller
|
||||
public class DevLabController {
|
||||
|
||||
@GetMapping("/devlab")
|
||||
public String showDevLab() {
|
||||
return "devlab";
|
||||
}
|
||||
|
||||
@GetMapping("/devlab/games/{slug}")
|
||||
public String showDevLabGame(@PathVariable String slug, Model model) {
|
||||
model.addAttribute("gameSlug", slug);
|
||||
return "devlab-game";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class DummyController {
|
||||
|
||||
@GetMapping("/comingsoon")
|
||||
public String comingSoon() {
|
||||
return "dummy"; // verweist auf dummy.html in src/main/resources/templates
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class FinalfluxController {
|
||||
|
||||
@GetMapping("/finalflux")
|
||||
public String showFinalfluxOverview() {
|
||||
return "finalflux"; // Lädt templates/finalflux.html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.dto.GameStats;
|
||||
import com.example.mindflux.dto.ScoreResult;
|
||||
import com.example.mindflux.dto.ScoreSubmission;
|
||||
import com.example.mindflux.service.GameScoreService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Generisches Score-API für alle Mindflux-Spiele. Der Spielname kommt aus der URL
|
||||
* und wird gegen eine Allowlist geprüft, damit keine beliebigen Spiele-Kennungen
|
||||
* in die Datenbank wandern.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/games")
|
||||
public class GameScoreController {
|
||||
|
||||
/** Bekannte Spiele (entsprechen den Dashboard-Routen). */
|
||||
private static final Set<String> KNOWN_GAMES = Set.of(
|
||||
"mindmatch", "techtacular", "syntax-saga", "devlab", "escape-room",
|
||||
"service-clash", "bug-hunter", "netzwerker");
|
||||
|
||||
private final GameScoreService gameScoreService;
|
||||
|
||||
public GameScoreController(GameScoreService gameScoreService) {
|
||||
this.gameScoreService = gameScoreService;
|
||||
}
|
||||
|
||||
@PostMapping("/{game}/scores")
|
||||
public ResponseEntity<ScoreResult> submitScore(@PathVariable String game,
|
||||
@RequestBody ScoreSubmission submission,
|
||||
Principal principal) {
|
||||
if (!KNOWN_GAMES.contains(game)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(gameScoreService.record(principal.getName(), game, submission));
|
||||
}
|
||||
|
||||
@GetMapping("/{game}/stats")
|
||||
public ResponseEntity<GameStats> getStats(@PathVariable String game, Principal principal) {
|
||||
if (!KNOWN_GAMES.contains(game)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(gameScoreService.getStats(principal.getName(), game));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class ImpressumController {
|
||||
|
||||
@GetMapping("/impressum")
|
||||
public String impressum() {
|
||||
return "impressum"; // verweist auf impressum.html in src/main/resources/templates
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
/**
|
||||
* Liefert die statischen Rechts-/Informationsseiten aus dem Footer:
|
||||
* AGB / Nutzungsbedingungen, Datenschutzerklärung und Community-Richtlinien.
|
||||
*/
|
||||
@Controller
|
||||
public class LegalController {
|
||||
|
||||
@GetMapping("/nutzungsbedingungen")
|
||||
public String nutzungsbedingungen() {
|
||||
return "legal/nutzungsbedingungen";
|
||||
}
|
||||
|
||||
@GetMapping("/datenschutz")
|
||||
public String datenschutz() {
|
||||
return "legal/datenschutz";
|
||||
}
|
||||
|
||||
@GetMapping("/richtlinien")
|
||||
public String richtlinien() {
|
||||
return "legal/richtlinien";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.dto.MindMatchDeckView;
|
||||
import com.example.mindflux.service.MindMatchService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Liefert die aktiven MindMatch-Decks (inkl. Paare) an die Spielseite. */
|
||||
@RestController
|
||||
@RequestMapping("/api/mindmatch")
|
||||
public class MindMatchApiController {
|
||||
|
||||
private final MindMatchService mindMatchService;
|
||||
|
||||
public MindMatchApiController(MindMatchService mindMatchService) {
|
||||
this.mindMatchService = mindMatchService;
|
||||
}
|
||||
|
||||
@GetMapping("/decks")
|
||||
public List<MindMatchDeckView> decks() {
|
||||
return mindMatchService.activeDecks();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class MindMatchController {
|
||||
|
||||
@GetMapping("/mindmatch")
|
||||
public String showMindMatch() {
|
||||
return "mindmatch"; // lädt templates/mindmatch.html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.image.BufferedImage;
|
||||
import javax.imageio.ImageIO;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.Principal;
|
||||
import java.util.UUID;
|
||||
|
||||
@Controller
|
||||
public class ProfileController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private BCryptPasswordEncoder passwordEncoder;
|
||||
|
||||
private static final String UPLOAD_DIR = System.getProperty("user.dir") + "/uploads/profiles/";
|
||||
|
||||
@GetMapping("/profile")
|
||||
public String profile(Model model, Principal principal, @RequestParam(value = "success", required = false) Boolean success) {
|
||||
String username = principal.getName();
|
||||
User user = userService.findByUsername(username);
|
||||
|
||||
model.addAttribute("user", user);
|
||||
|
||||
if (Boolean.TRUE.equals(success)) {
|
||||
model.addAttribute("message", "Profil erfolgreich aktualisiert!");
|
||||
}
|
||||
|
||||
return "profile";
|
||||
}
|
||||
|
||||
@PostMapping("/profile/delete-picture")
|
||||
public String deleteProfilePicture(Principal principal) {
|
||||
String username = principal.getName();
|
||||
User existingUser = userService.findByUsername(username);
|
||||
|
||||
if (existingUser != null) {
|
||||
String profilePicture = existingUser.getProfilePicture();
|
||||
if (profilePicture != null && !profilePicture.trim().isEmpty()) {
|
||||
try {
|
||||
Path picturePath = Paths.get(System.getProperty("user.dir"), "uploads", "profiles", profilePicture);
|
||||
System.out.println("Versuche zu löschen: " + picturePath.toAbsolutePath());
|
||||
|
||||
if (Files.exists(picturePath)) {
|
||||
Files.delete(picturePath);
|
||||
System.out.println("Datei erfolgreich gelöscht: " + picturePath.getFileName());
|
||||
} else {
|
||||
System.out.println("Datei existiert nicht: " + picturePath.toAbsolutePath());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("Fehler beim Löschen der Datei: " + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
System.out.println("Kein Profilbild gesetzt, nichts zu löschen.");
|
||||
}
|
||||
|
||||
existingUser.setProfilePicture(null); // Wichtig: auch wenn Datei nicht existiert
|
||||
userService.save(existingUser);
|
||||
}
|
||||
|
||||
return "redirect:/profile?success=true";
|
||||
}
|
||||
|
||||
@GetMapping("/profile/knowledge")
|
||||
public String knowledgeCenter(Model model, Principal principal) {
|
||||
String username = principal.getName();
|
||||
User user = userService.findByUsername(username);
|
||||
|
||||
model.addAttribute("user", user);
|
||||
return "knowledge";
|
||||
}
|
||||
|
||||
@PostMapping("/profile/update")
|
||||
public String updateProfile(@RequestParam("email") String email,
|
||||
@RequestParam(value = "profilePicture", required = false) MultipartFile file,
|
||||
Principal principal) {
|
||||
String username = principal.getName();
|
||||
User existingUser = userService.findByUsername(username);
|
||||
|
||||
if (existingUser != null) {
|
||||
existingUser.setEmail(email); // Email aktualisieren
|
||||
|
||||
if (file != null && !file.isEmpty()) {
|
||||
try {
|
||||
BufferedImage original = ImageIO.read(file.getInputStream());
|
||||
if (original == null) {
|
||||
// Keine lesbare Bilddatei -> Upload ignorieren, restliches Profil trotzdem speichern.
|
||||
System.out.println("Profilbild-Upload ignoriert: keine lesbare Bilddatei.");
|
||||
} else {
|
||||
// Auf max. Kantenlänge herunterskalieren (kein Hochskalieren).
|
||||
final int MAX_DIM = 400;
|
||||
int w = original.getWidth();
|
||||
int h = original.getHeight();
|
||||
double scale = Math.min(1.0, (double) MAX_DIM / Math.max(w, h));
|
||||
int nw = Math.max(1, (int) Math.round(w * scale));
|
||||
int nh = Math.max(1, (int) Math.round(h * scale));
|
||||
|
||||
BufferedImage scaled = new BufferedImage(nw, nh, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g = scaled.createGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
g.setColor(Color.WHITE); // JPEG kennt kein Alpha -> Transparenz auf weißem Grund
|
||||
g.fillRect(0, 0, nw, nh);
|
||||
g.drawImage(original, 0, 0, nw, nh, null);
|
||||
g.dispose();
|
||||
|
||||
// Altes Profilbild löschen (falls vorhanden)
|
||||
if (existingUser.getProfilePicture() != null && !existingUser.getProfilePicture().isEmpty()) {
|
||||
Path oldFilePath = Paths.get(System.getProperty("user.dir"), "uploads", "profiles", existingUser.getProfilePicture());
|
||||
Files.deleteIfExists(oldFilePath);
|
||||
}
|
||||
|
||||
// Normalisiert als JPEG speichern
|
||||
String fileName = UUID.randomUUID() + ".jpg";
|
||||
Path uploadPath = Paths.get(UPLOAD_DIR);
|
||||
Files.createDirectories(uploadPath);
|
||||
ImageIO.write(scaled, "jpg", uploadPath.resolve(fileName).toFile());
|
||||
|
||||
existingUser.setProfilePicture(fileName);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
userService.save(existingUser);
|
||||
}
|
||||
|
||||
return "redirect:/profile?success=true";
|
||||
}
|
||||
|
||||
@PostMapping("/profile/change-password")
|
||||
public String changePassword(@RequestParam("oldPassword") String oldPassword,
|
||||
@RequestParam("newPassword") String newPassword,
|
||||
Principal principal,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
String username = principal.getName();
|
||||
User user = userService.findByUsername(username);
|
||||
|
||||
if (user == null) {
|
||||
redirectAttributes.addFlashAttribute("error", "Benutzer nicht gefunden.");
|
||||
return "redirect:/profile/settings";
|
||||
}
|
||||
|
||||
if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
|
||||
redirectAttributes.addFlashAttribute("error", "Das aktuelle Passwort ist falsch.");
|
||||
return "redirect:/profile/settings";
|
||||
}
|
||||
|
||||
user.setPassword(passwordEncoder.encode(newPassword));
|
||||
userService.save(user);
|
||||
|
||||
redirectAttributes.addFlashAttribute("message", "Passwort erfolgreich geändert.");
|
||||
return "redirect:/profile/settings";
|
||||
}
|
||||
|
||||
@PostMapping("/profile/delete")
|
||||
public String deleteProfile(Principal principal, Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
|
||||
String username = principal.getName();
|
||||
|
||||
// Profilbild-Datei von der Platte entfernen (DB-Daten löscht der Service in einer Transaktion).
|
||||
User user = userService.findByUsername(username);
|
||||
if (user != null) {
|
||||
String profilePicture = user.getProfilePicture();
|
||||
if (profilePicture != null && !profilePicture.trim().isEmpty()) {
|
||||
try {
|
||||
Path picturePath = Paths.get(System.getProperty("user.dir"), "uploads", "profiles", profilePicture);
|
||||
Files.deleteIfExists(picturePath);
|
||||
} catch (IOException e) {
|
||||
System.err.println("Fehler beim Löschen des Profilbilds: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userService.deleteByUsername(username);
|
||||
new SecurityContextLogoutHandler().logout(request, response, authentication);
|
||||
return "redirect:/?logout";
|
||||
}
|
||||
|
||||
// Hilfsmethode: Dateiendung extrahieren
|
||||
private String getFileExtension(String filename) {
|
||||
if (filename == null || filename.lastIndexOf(".") == -1) {
|
||||
return "";
|
||||
}
|
||||
return filename.substring(filename.lastIndexOf("."));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.dto.GameProgress;
|
||||
import com.example.mindflux.dto.ProgressData;
|
||||
import com.example.mindflux.service.ProgressService;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
public class ProgressController {
|
||||
|
||||
@Autowired
|
||||
private ProgressService progressService;
|
||||
|
||||
@GetMapping("/progress") // <-- hier geändert!
|
||||
public String showProgress(Model model, Principal principal) {
|
||||
String username = principal.getName(); // aktueller eingeloggter Benutzer
|
||||
|
||||
ProgressData progress = progressService.getOverallProgress(username);
|
||||
List<GameProgress> games = progressService.getGameProgress(username);
|
||||
|
||||
if (progress == null) {
|
||||
// Falls keine Fortschrittsdaten vorhanden sind, Dummy-Daten setzen
|
||||
model.addAttribute("correctAnswers", 0);
|
||||
model.addAttribute("currentXp", 0);
|
||||
model.addAttribute("xpForNextLevel", 1000);
|
||||
model.addAttribute("userLevel", 1);
|
||||
model.addAttribute("xpPercentage", 0);
|
||||
} else {
|
||||
model.addAttribute("correctAnswers", progress.getCorrectAnswers());
|
||||
model.addAttribute("currentXp", progress.getCurrentXp());
|
||||
model.addAttribute("xpForNextLevel", progress.getXpForNextLevel());
|
||||
model.addAttribute("userLevel", progress.getUserLevel());
|
||||
model.addAttribute("xpPercentage", progress.getProgressPercent());
|
||||
}
|
||||
|
||||
model.addAttribute("gameProgress", games != null ? games : List.of()); // leere Liste, falls null
|
||||
|
||||
return "progress"; // ruft progress.html auf
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Controller
|
||||
public class RegisterController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@GetMapping("/register")
|
||||
public String showRegistrationForm(Model model) {
|
||||
model.addAttribute("user", new User());
|
||||
return "register";
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
public String processRegistration(@ModelAttribute("user") User user, Model model) {
|
||||
// Benutzername klein schreiben, um Doppelungen zu vermeiden
|
||||
user.setUsername(user.getUsername().toLowerCase());
|
||||
|
||||
// Prüfen, ob Benutzername schon existiert
|
||||
if (userService.findByUsername(user.getUsername()) != null) {
|
||||
// Zurück zur kombinierten Login/Signup-Seite mit geöffnetem Signup-Panel
|
||||
return "redirect:/?registerError";
|
||||
}
|
||||
|
||||
// Passwort verschlüsseln
|
||||
user.setPassword(passwordEncoder.encode(user.getPassword()));
|
||||
user.setRole("USER"); // Standardrolle
|
||||
userService.save(user);
|
||||
|
||||
return "redirect:/?registerSuccess"; // zurück zum Login mit Erfolgsmeldung
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class SettingsController {
|
||||
|
||||
@GetMapping("/profile/settings")
|
||||
public String showSettingsPage() {
|
||||
return "settings"; // lädt templates/settings.html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class SozialesController {
|
||||
|
||||
@GetMapping("/soziales")
|
||||
public String soziales() {
|
||||
return "soziales"; // -> templates/soziales.html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.dto.SyntaxSagaCheckRequest;
|
||||
import com.example.mindflux.dto.SyntaxSagaCheckResult;
|
||||
import com.example.mindflux.dto.SyntaxSagaFinishRequest;
|
||||
import com.example.mindflux.dto.SyntaxSagaResult;
|
||||
import com.example.mindflux.dto.SyntaxSagaStoryView;
|
||||
import com.example.mindflux.service.SyntaxSagaService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
/** JSON-API für Syntax Saga. Server-autoritativ: Lösungen werden nie ausgeliefert. */
|
||||
@RestController
|
||||
@RequestMapping("/api/syntaxsaga")
|
||||
public class SyntaxSagaApiController {
|
||||
|
||||
private final SyntaxSagaService syntaxSagaService;
|
||||
|
||||
public SyntaxSagaApiController(SyntaxSagaService syntaxSagaService) {
|
||||
this.syntaxSagaService = syntaxSagaService;
|
||||
}
|
||||
|
||||
/** Missionen einer Stufe (oder stufenübergreifend) – ohne Lösungen. */
|
||||
@GetMapping("/story")
|
||||
public SyntaxSagaStoryView story(@RequestParam(required = false) String difficulty) {
|
||||
return syntaxSagaService.buildStory(difficulty);
|
||||
}
|
||||
|
||||
/** Eine Mission serverseitig prüfen; mit {@code reveal=true} auch die Auflösung. */
|
||||
@PostMapping("/check")
|
||||
public SyntaxSagaCheckResult check(@RequestBody SyntaxSagaCheckRequest request) {
|
||||
return syntaxSagaService.check(request);
|
||||
}
|
||||
|
||||
/** Durchlauf abschließen: Score wird serverseitig neu berechnet und gespeichert. */
|
||||
@PostMapping("/finish")
|
||||
public SyntaxSagaResult finish(@RequestBody SyntaxSagaFinishRequest request, Principal principal) {
|
||||
return syntaxSagaService.finish(principal.getName(), request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class SyntaxSagaController {
|
||||
|
||||
@GetMapping("/syntaxsaga")
|
||||
public String show() {
|
||||
return "syntaxsaga"; // Lädt templates/syntaxsaga.html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.dto.*;
|
||||
import com.example.mindflux.service.TechtacularService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/** Spieler-API für TechTacular: Board laden, Antwort prüfen, Runde abschließen. */
|
||||
@RestController
|
||||
@RequestMapping("/api/techtacular")
|
||||
public class TechtacularApiController {
|
||||
|
||||
private final TechtacularService techtacularService;
|
||||
|
||||
public TechtacularApiController(TechtacularService techtacularService) {
|
||||
this.techtacularService = techtacularService;
|
||||
}
|
||||
|
||||
/** exclude = komma-separierte Frage-IDs der Vorrunde, die möglichst nicht erneut kommen sollen. */
|
||||
@GetMapping("/board")
|
||||
public TechtacularBoardView board(@RequestParam(required = false) String exclude) {
|
||||
return techtacularService.buildBoard(parseIds(exclude));
|
||||
}
|
||||
|
||||
private Set<Long> parseIds(String csv) {
|
||||
Set<Long> ids = new HashSet<>();
|
||||
if (csv != null && !csv.isBlank()) {
|
||||
for (String part : csv.split(",")) {
|
||||
try {
|
||||
ids.add(Long.parseLong(part.trim()));
|
||||
} catch (NumberFormatException ignored) {
|
||||
// ungültige IDs einfach überspringen
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
@PostMapping("/answer")
|
||||
public AnswerResult answer(@RequestBody AnswerRequest request) {
|
||||
return techtacularService.checkAnswer(request.getQuestionId(), request.getAnswerId());
|
||||
}
|
||||
|
||||
@PostMapping("/finish")
|
||||
public TechtacularResult finish(@RequestBody FinishRequest request, Principal principal) {
|
||||
return techtacularService.finish(principal.getName(), request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class TechtacularController {
|
||||
|
||||
@GetMapping("/techtacular")
|
||||
public String showTechtacular() {
|
||||
return "techtacular"; // lädt templates/techtacular.html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.repository.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
// 📥 User erstellen
|
||||
@PostMapping
|
||||
public User createUser(@RequestBody User user) {
|
||||
user.setPassword(passwordEncoder.encode(user.getPassword()));
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
// 📋 Alle User anzeigen
|
||||
@GetMapping
|
||||
public List<User> getAllUsers() {
|
||||
return userRepository.findAll();
|
||||
}
|
||||
|
||||
// 📍 Einzelnen User anzeigen
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<User> getUserById(@PathVariable Long id) {
|
||||
Optional<User> user = userRepository.findById(id);
|
||||
return user.map(ResponseEntity::ok)
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
// ✏️ User aktualisieren
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User updatedUser) {
|
||||
return userRepository.findById(id).map(user -> {
|
||||
user.setUsername(updatedUser.getUsername());
|
||||
if (updatedUser.getPassword() != null && !updatedUser.getPassword().isBlank()) {
|
||||
user.setPassword(passwordEncoder.encode(updatedUser.getPassword()));
|
||||
}
|
||||
user.setEmail(updatedUser.getEmail());
|
||||
user.setRole(updatedUser.getRole());
|
||||
return ResponseEntity.ok(userRepository.save(user));
|
||||
}).orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
// 🗑️ User löschen
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
|
||||
if (userRepository.existsById(id)) {
|
||||
userRepository.deleteById(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.example.mindflux.controller;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.repository.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/users")
|
||||
public class UserViewController {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
// Formular anzeigen
|
||||
@GetMapping("/form")
|
||||
public String showForm(Model model) {
|
||||
model.addAttribute("user", new User());
|
||||
return "user-form";
|
||||
}
|
||||
|
||||
// Benutzer speichern
|
||||
@PostMapping("/save")
|
||||
public String saveUser(@ModelAttribute User user) {
|
||||
userRepository.save(user);
|
||||
return "user-success"; // statt redirect
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Eine Zeile in der TechTacular-Kategorienübersicht (ohne alle Fragen zu laden). */
|
||||
public class AdminCategoryRow {
|
||||
private final Long id;
|
||||
private final String slug;
|
||||
private final String title;
|
||||
private final boolean active;
|
||||
private final long questionCount;
|
||||
|
||||
public AdminCategoryRow(Long id, String slug, String title, boolean active, long questionCount) {
|
||||
this.id = id;
|
||||
this.slug = slug;
|
||||
this.title = title;
|
||||
this.active = active;
|
||||
this.questionCount = questionCount;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public String getSlug() { return slug; }
|
||||
public String getTitle() { return title; }
|
||||
public boolean isActive() { return active; }
|
||||
public long getQuestionCount() { return questionCount; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Eine vom Spieler gewählte Antwort. */
|
||||
public class AnswerRequest {
|
||||
private Long questionId;
|
||||
private Long answerId;
|
||||
|
||||
public Long getQuestionId() { return questionId; }
|
||||
public void setQuestionId(Long questionId) { this.questionId = questionId; }
|
||||
|
||||
public Long getAnswerId() { return answerId; }
|
||||
public void setAnswerId(Long answerId) { this.answerId = answerId; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Serverseitige Bewertung einer einzelnen Antwort (instant feedback). */
|
||||
public class AnswerResult {
|
||||
private final boolean correct;
|
||||
private final Long correctAnswerId;
|
||||
private final String explanation;
|
||||
private final int points;
|
||||
|
||||
public AnswerResult(boolean correct, Long correctAnswerId, String explanation, int points) {
|
||||
this.correct = correct;
|
||||
this.correctAnswerId = correctAnswerId;
|
||||
this.explanation = explanation;
|
||||
this.points = points;
|
||||
}
|
||||
|
||||
public boolean isCorrect() { return correct; }
|
||||
public Long getCorrectAnswerId() { return correctAnswerId; }
|
||||
public String getExplanation() { return explanation; }
|
||||
public int getPoints() { return points; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
/** Prüft eine Dev-Lab-Aufgabe. {@code submission} ist mechanikspezifisch (JSON). */
|
||||
public class DevLabCheckRequest {
|
||||
|
||||
private Long taskId;
|
||||
private boolean reveal;
|
||||
private JsonNode submission;
|
||||
|
||||
public Long getTaskId() { return taskId; }
|
||||
public void setTaskId(Long taskId) { this.taskId = taskId; }
|
||||
|
||||
public boolean isReveal() { return reveal; }
|
||||
public void setReveal(boolean reveal) { this.reveal = reveal; }
|
||||
|
||||
public JsonNode getSubmission() { return submission; }
|
||||
public void setSubmission(JsonNode submission) { this.submission = submission; }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Abschluss eines Dev-Lab-Durchlaufs: pro Aufgabe die letzte Eingabe + Versuche/Auflösung.
|
||||
* Der Score wird serverseitig aus den Eingaben neu berechnet.
|
||||
*/
|
||||
public class DevLabFinishRequest {
|
||||
|
||||
private String slug;
|
||||
private int durationSeconds;
|
||||
private List<Item> items;
|
||||
|
||||
public String getSlug() { return slug; }
|
||||
public void setSlug(String slug) { this.slug = slug; }
|
||||
|
||||
public int getDurationSeconds() { return durationSeconds; }
|
||||
public void setDurationSeconds(int durationSeconds) { this.durationSeconds = durationSeconds; }
|
||||
|
||||
public List<Item> getItems() { return items; }
|
||||
public void setItems(List<Item> items) { this.items = items; }
|
||||
|
||||
public static class Item {
|
||||
private Long taskId;
|
||||
private JsonNode submission;
|
||||
private int attempts;
|
||||
private boolean revealed;
|
||||
|
||||
public Long getTaskId() { return taskId; }
|
||||
public void setTaskId(Long taskId) { this.taskId = taskId; }
|
||||
|
||||
public JsonNode getSubmission() { return submission; }
|
||||
public void setSubmission(JsonNode submission) { this.submission = submission; }
|
||||
|
||||
public int getAttempts() { return attempts; }
|
||||
public void setAttempts(int attempts) { this.attempts = attempts; }
|
||||
|
||||
public boolean isRevealed() { return revealed; }
|
||||
public void setRevealed(boolean revealed) { this.revealed = revealed; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Eine Zeile in der Admin-Spieleübersicht (ohne alle Aufgaben zu laden). */
|
||||
public class DevLabGameRow {
|
||||
private final Long id;
|
||||
private final String slug;
|
||||
private final String title;
|
||||
private final String language;
|
||||
private final String path;
|
||||
private final boolean active;
|
||||
private final long taskCount;
|
||||
|
||||
public DevLabGameRow(Long id, String slug, String title, String language, String path,
|
||||
boolean active, long taskCount) {
|
||||
this.id = id;
|
||||
this.slug = slug;
|
||||
this.title = title;
|
||||
this.language = language;
|
||||
this.path = path;
|
||||
this.active = active;
|
||||
this.taskCount = taskCount;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public String getSlug() { return slug; }
|
||||
public String getTitle() { return title; }
|
||||
public String getLanguage() { return language; }
|
||||
public String getPath() { return path; }
|
||||
public boolean isActive() { return active; }
|
||||
public long getTaskCount() { return taskCount; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Endergebnis eines Dev-Lab-Durchlaufs (Score serverseitig berechnet) + Highscore-Stand. */
|
||||
public class DevLabResult {
|
||||
|
||||
private final int score;
|
||||
private final int maxScore;
|
||||
private final int solvedTasks;
|
||||
private final int totalTasks;
|
||||
private final int stars;
|
||||
private final int highscore;
|
||||
private final boolean newHighscore;
|
||||
private final long plays;
|
||||
|
||||
public DevLabResult(int score, int maxScore, int solvedTasks, int totalTasks, int stars,
|
||||
int highscore, boolean newHighscore, long plays) {
|
||||
this.score = score;
|
||||
this.maxScore = maxScore;
|
||||
this.solvedTasks = solvedTasks;
|
||||
this.totalTasks = totalTasks;
|
||||
this.stars = stars;
|
||||
this.highscore = highscore;
|
||||
this.newHighscore = newHighscore;
|
||||
this.plays = plays;
|
||||
}
|
||||
|
||||
public int getScore() { return score; }
|
||||
public int getMaxScore() { return maxScore; }
|
||||
public int getSolvedTasks() { return solvedTasks; }
|
||||
public int getTotalTasks() { return totalTasks; }
|
||||
public int getStars() { return stars; }
|
||||
public int getHighscore() { return highscore; }
|
||||
public boolean isNewHighscore() { return newHighscore; }
|
||||
public long getPlays() { return plays; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Abschluss einer TechTacular-Runde: alle gewählten Antworten + Dauer. Score wird serverseitig recomputed. */
|
||||
public class FinishRequest {
|
||||
private int durationSeconds;
|
||||
private List<AnswerRequest> answers;
|
||||
|
||||
public int getDurationSeconds() { return durationSeconds; }
|
||||
public void setDurationSeconds(int durationSeconds) { this.durationSeconds = durationSeconds; }
|
||||
|
||||
public List<AnswerRequest> getAnswers() { return answers; }
|
||||
public void setAnswers(List<AnswerRequest> answers) { this.answers = answers; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public class GameProgress {
|
||||
private String name;
|
||||
private int progressPercent;
|
||||
private int highscore;
|
||||
private int plays;
|
||||
private LocalDate lastPlayed;
|
||||
|
||||
// Getter
|
||||
public String getName() { return name; }
|
||||
public int getProgressPercent() { return progressPercent; }
|
||||
public int getHighscore() { return highscore; }
|
||||
public int getPlays() { return plays; }
|
||||
public LocalDate getLastPlayed() { return lastPlayed; }
|
||||
|
||||
// Setter
|
||||
public void setName(String name) { this.name = name; }
|
||||
public void setProgressPercent(int progressPercent) { this.progressPercent = progressPercent; }
|
||||
public void setHighscore(int highscore) { this.highscore = highscore; }
|
||||
public void setPlays(int plays) { this.plays = plays; }
|
||||
public void setLastPlayed(LocalDate lastPlayed) { this.lastPlayed = lastPlayed; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/** Zusammengefasste Statistik eines Spielers für ein Spiel. */
|
||||
public class GameStats {
|
||||
private final int highscore;
|
||||
private final long plays;
|
||||
private final LocalDate lastPlayed;
|
||||
|
||||
public GameStats(int highscore, long plays, LocalDate lastPlayed) {
|
||||
this.highscore = highscore;
|
||||
this.plays = plays;
|
||||
this.lastPlayed = lastPlayed;
|
||||
}
|
||||
|
||||
public int getHighscore() { return highscore; }
|
||||
public long getPlays() { return plays; }
|
||||
public LocalDate getLastPlayed() { return lastPlayed; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Ein aktives Deck mit seinen Paaren fürs Spiel-API. */
|
||||
public class MindMatchDeckView {
|
||||
private final String slug;
|
||||
private final String title;
|
||||
private final List<MindMatchPairView> pairs;
|
||||
|
||||
public MindMatchDeckView(String slug, String title, List<MindMatchPairView> pairs) {
|
||||
this.slug = slug;
|
||||
this.title = title;
|
||||
this.pairs = pairs;
|
||||
}
|
||||
|
||||
public String getSlug() { return slug; }
|
||||
public String getTitle() { return title; }
|
||||
public List<MindMatchPairView> getPairs() { return pairs; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Ein Paar fürs Spiel-API. Feld heißt "match" (passend zur bisherigen JS-Logik). */
|
||||
public class MindMatchPairView {
|
||||
private final String term;
|
||||
private final String match;
|
||||
|
||||
public MindMatchPairView(String term, String match) {
|
||||
this.term = term;
|
||||
this.match = match;
|
||||
}
|
||||
|
||||
public String getTerm() { return term; }
|
||||
public String getMatch() { return match; }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
public class ProgressData {
|
||||
private int correctAnswers;
|
||||
private int currentXp;
|
||||
private int xpForNextLevel;
|
||||
private int userLevel;
|
||||
private int progressPercent;
|
||||
|
||||
public int getCorrectAnswers() {
|
||||
return correctAnswers;
|
||||
}
|
||||
|
||||
public void setCorrectAnswers(int correctAnswers) {
|
||||
this.correctAnswers = correctAnswers;
|
||||
}
|
||||
|
||||
public int getCurrentXp() {
|
||||
return currentXp;
|
||||
}
|
||||
|
||||
public void setCurrentXp(int currentXp) {
|
||||
this.currentXp = currentXp;
|
||||
}
|
||||
|
||||
public int getXpForNextLevel() {
|
||||
return xpForNextLevel;
|
||||
}
|
||||
|
||||
public void setXpForNextLevel(int xpForNextLevel) {
|
||||
this.xpForNextLevel = xpForNextLevel;
|
||||
}
|
||||
|
||||
public int getUserLevel() {
|
||||
return userLevel;
|
||||
}
|
||||
|
||||
public void setUserLevel(int userLevel) {
|
||||
this.userLevel = userLevel;
|
||||
}
|
||||
|
||||
public int getProgressPercent() {
|
||||
return progressPercent;
|
||||
}
|
||||
|
||||
public void setProgressPercent(int progressPercent) {
|
||||
this.progressPercent = progressPercent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Antwort nach dem Speichern eines Ergebnisses. */
|
||||
public class ScoreResult {
|
||||
private final int score;
|
||||
private final int highscore;
|
||||
private final boolean newHighscore;
|
||||
private final long plays;
|
||||
|
||||
public ScoreResult(int score, int highscore, boolean newHighscore, long plays) {
|
||||
this.score = score;
|
||||
this.highscore = highscore;
|
||||
this.newHighscore = newHighscore;
|
||||
this.plays = plays;
|
||||
}
|
||||
|
||||
public int getScore() { return score; }
|
||||
public int getHighscore() { return highscore; }
|
||||
public boolean isNewHighscore() { return newHighscore; }
|
||||
public long getPlays() { return plays; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/**
|
||||
* Was der Client nach einem abgeschlossenen Spiel sendet. Der Score wird
|
||||
* bewusst NICHT mitgeschickt, sondern serverseitig aus diesen Rohwerten
|
||||
* berechnet, damit ein manipulierter Client keinen Fantasie-Score speichern kann.
|
||||
*/
|
||||
public class ScoreSubmission {
|
||||
private String deck;
|
||||
private int pairs;
|
||||
private int moves;
|
||||
private int durationSeconds;
|
||||
|
||||
public String getDeck() { return deck; }
|
||||
public void setDeck(String deck) { this.deck = deck; }
|
||||
|
||||
public int getPairs() { return pairs; }
|
||||
public void setPairs(int pairs) { this.pairs = pairs; }
|
||||
|
||||
public int getMoves() { return moves; }
|
||||
public void setMoves(int moves) { this.moves = moves; }
|
||||
|
||||
public int getDurationSeconds() { return durationSeconds; }
|
||||
public void setDurationSeconds(int durationSeconds) { this.durationSeconds = durationSeconds; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Eine Mission prüfen: die eingetippten Lücken (+ ob die Auflösung angefordert wird). */
|
||||
public class SyntaxSagaCheckRequest {
|
||||
|
||||
private Long missionId;
|
||||
private boolean reveal;
|
||||
private List<GapAnswer> answers;
|
||||
|
||||
public Long getMissionId() { return missionId; }
|
||||
public void setMissionId(Long missionId) { this.missionId = missionId; }
|
||||
|
||||
public boolean isReveal() { return reveal; }
|
||||
public void setReveal(boolean reveal) { this.reveal = reveal; }
|
||||
|
||||
public List<GapAnswer> getAnswers() { return answers; }
|
||||
public void setAnswers(List<GapAnswer> answers) { this.answers = answers; }
|
||||
|
||||
public static class GapAnswer {
|
||||
private int position;
|
||||
private String text;
|
||||
|
||||
public int getPosition() { return position; }
|
||||
public void setPosition(int position) { this.position = position; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Serverseitige Bewertung einer Mission (instant feedback). {@code solution} ist nur
|
||||
* gefüllt, wenn die Auflösung angefordert wurde und die Lücke nicht korrekt war.
|
||||
*/
|
||||
public class SyntaxSagaCheckResult {
|
||||
|
||||
private final boolean allCorrect;
|
||||
private final List<GapVerdict> results;
|
||||
|
||||
public SyntaxSagaCheckResult(boolean allCorrect, List<GapVerdict> results) {
|
||||
this.allCorrect = allCorrect;
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
public boolean isAllCorrect() { return allCorrect; }
|
||||
public List<GapVerdict> getResults() { return results; }
|
||||
|
||||
public static class GapVerdict {
|
||||
private final int position;
|
||||
private final boolean correct;
|
||||
private final String solution;
|
||||
|
||||
public GapVerdict(int position, boolean correct, String solution) {
|
||||
this.position = position;
|
||||
this.correct = correct;
|
||||
this.solution = solution;
|
||||
}
|
||||
|
||||
public int getPosition() { return position; }
|
||||
public boolean isCorrect() { return correct; }
|
||||
public String getSolution() { return solution; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Abschluss eines Saga-Durchlaufs: alle eingetippten Lücken über alle Missionen einer
|
||||
* Stufe (+ Versuche und ob aufgelöst wurde). Der Score wird serverseitig neu berechnet.
|
||||
*/
|
||||
public class SyntaxSagaFinishRequest {
|
||||
|
||||
private String difficulty;
|
||||
private int durationSeconds;
|
||||
private List<FinishGap> gaps;
|
||||
|
||||
public String getDifficulty() { return difficulty; }
|
||||
public void setDifficulty(String difficulty) { this.difficulty = difficulty; }
|
||||
|
||||
public int getDurationSeconds() { return durationSeconds; }
|
||||
public void setDurationSeconds(int durationSeconds) { this.durationSeconds = durationSeconds; }
|
||||
|
||||
public List<FinishGap> getGaps() { return gaps; }
|
||||
public void setGaps(List<FinishGap> gaps) { this.gaps = gaps; }
|
||||
|
||||
public static class FinishGap {
|
||||
private Long missionId;
|
||||
private int position;
|
||||
private String text;
|
||||
private int attempts;
|
||||
private boolean revealed;
|
||||
|
||||
public Long getMissionId() { return missionId; }
|
||||
public void setMissionId(Long missionId) { this.missionId = missionId; }
|
||||
|
||||
public int getPosition() { return position; }
|
||||
public void setPosition(int position) { this.position = position; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
||||
public int getAttempts() { return attempts; }
|
||||
public void setAttempts(int attempts) { this.attempts = attempts; }
|
||||
|
||||
public boolean isRevealed() { return revealed; }
|
||||
public void setRevealed(boolean revealed) { this.revealed = revealed; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Eine Zeile in der Admin-Missionsübersicht (ohne alle Lücken zu laden). */
|
||||
public class SyntaxSagaMissionRow {
|
||||
private final Long id;
|
||||
private final String slug;
|
||||
private final String title;
|
||||
private final String difficulty;
|
||||
private final boolean active;
|
||||
private final long gapCount;
|
||||
|
||||
public SyntaxSagaMissionRow(Long id, String slug, String title, String difficulty,
|
||||
boolean active, long gapCount) {
|
||||
this.id = id;
|
||||
this.slug = slug;
|
||||
this.title = title;
|
||||
this.difficulty = difficulty;
|
||||
this.active = active;
|
||||
this.gapCount = gapCount;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public String getSlug() { return slug; }
|
||||
public String getTitle() { return title; }
|
||||
public String getDifficulty() { return difficulty; }
|
||||
public boolean isActive() { return active; }
|
||||
public long getGapCount() { return gapCount; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Endergebnis eines Saga-Durchlaufs (Score serverseitig berechnet) + Highscore-Stand. */
|
||||
public class SyntaxSagaResult {
|
||||
|
||||
private final int score;
|
||||
private final int maxScore;
|
||||
private final int solvedGaps;
|
||||
private final int totalGaps;
|
||||
private final int stars;
|
||||
private final int highscore;
|
||||
private final boolean newHighscore;
|
||||
private final long plays;
|
||||
|
||||
public SyntaxSagaResult(int score, int maxScore, int solvedGaps, int totalGaps, int stars,
|
||||
int highscore, boolean newHighscore, long plays) {
|
||||
this.score = score;
|
||||
this.maxScore = maxScore;
|
||||
this.solvedGaps = solvedGaps;
|
||||
this.totalGaps = totalGaps;
|
||||
this.stars = stars;
|
||||
this.highscore = highscore;
|
||||
this.newHighscore = newHighscore;
|
||||
this.plays = plays;
|
||||
}
|
||||
|
||||
public int getScore() { return score; }
|
||||
public int getMaxScore() { return maxScore; }
|
||||
public int getSolvedGaps() { return solvedGaps; }
|
||||
public int getTotalGaps() { return totalGaps; }
|
||||
public int getStars() { return stars; }
|
||||
public int getHighscore() { return highscore; }
|
||||
public boolean isNewHighscore() { return newHighscore; }
|
||||
public long getPlays() { return plays; }
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Die Missionen einer Schwierigkeitsstufe fürs Frontend. Bewusst OHNE Lösungen –
|
||||
* geprüft und gewertet wird ausschließlich serverseitig.
|
||||
*/
|
||||
public class SyntaxSagaStoryView {
|
||||
|
||||
private final String difficulty;
|
||||
private final List<Mission> missions;
|
||||
private final int maxScore;
|
||||
private final int totalGaps;
|
||||
|
||||
public SyntaxSagaStoryView(String difficulty, List<Mission> missions, int maxScore, int totalGaps) {
|
||||
this.difficulty = difficulty;
|
||||
this.missions = missions;
|
||||
this.maxScore = maxScore;
|
||||
this.totalGaps = totalGaps;
|
||||
}
|
||||
|
||||
public String getDifficulty() { return difficulty; }
|
||||
public List<Mission> getMissions() { return missions; }
|
||||
public int getMaxScore() { return maxScore; }
|
||||
public int getTotalGaps() { return totalGaps; }
|
||||
|
||||
public static class Mission {
|
||||
private final Long missionId;
|
||||
private final String title;
|
||||
private final String difficulty;
|
||||
private final String language;
|
||||
private final String narrative;
|
||||
private final String dialog;
|
||||
private final String codeTemplate;
|
||||
private final String successText;
|
||||
private final List<Gap> gaps;
|
||||
|
||||
public Mission(Long missionId, String title, String difficulty, String language,
|
||||
String narrative, String dialog, String codeTemplate,
|
||||
String successText, List<Gap> gaps) {
|
||||
this.missionId = missionId;
|
||||
this.title = title;
|
||||
this.difficulty = difficulty;
|
||||
this.language = language;
|
||||
this.narrative = narrative;
|
||||
this.dialog = dialog;
|
||||
this.codeTemplate = codeTemplate;
|
||||
this.successText = successText;
|
||||
this.gaps = gaps;
|
||||
}
|
||||
|
||||
public Long getMissionId() { return missionId; }
|
||||
public String getTitle() { return title; }
|
||||
public String getDifficulty() { return difficulty; }
|
||||
public String getLanguage() { return language; }
|
||||
public String getNarrative() { return narrative; }
|
||||
public String getDialog() { return dialog; }
|
||||
public String getCodeTemplate() { return codeTemplate; }
|
||||
public String getSuccessText() { return successText; }
|
||||
public List<Gap> getGaps() { return gaps; }
|
||||
}
|
||||
|
||||
public static class Gap {
|
||||
private final int position;
|
||||
private final int points;
|
||||
private final String hint;
|
||||
private final String placeholder;
|
||||
|
||||
public Gap(int position, int points, String hint, String placeholder) {
|
||||
this.position = position;
|
||||
this.points = points;
|
||||
this.hint = hint;
|
||||
this.placeholder = placeholder;
|
||||
}
|
||||
|
||||
public int getPosition() { return position; }
|
||||
public int getPoints() { return points; }
|
||||
public String getHint() { return hint; }
|
||||
public String getPlaceholder() { return placeholder; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Das Jeopardy-Board fürs Frontend: Spalten (Kategorien) mit Kacheln (Fragen).
|
||||
* Bewusst OHNE Information darüber, welche Antwort korrekt ist – geprüft wird serverseitig.
|
||||
*/
|
||||
public class TechtacularBoardView {
|
||||
|
||||
private final List<Category> categories;
|
||||
private final int maxScore;
|
||||
|
||||
public TechtacularBoardView(List<Category> categories, int maxScore) {
|
||||
this.categories = categories;
|
||||
this.maxScore = maxScore;
|
||||
}
|
||||
|
||||
public List<Category> getCategories() { return categories; }
|
||||
public int getMaxScore() { return maxScore; }
|
||||
|
||||
public static class Category {
|
||||
private final String title;
|
||||
private final List<Tile> tiles;
|
||||
|
||||
public Category(String title, List<Tile> tiles) {
|
||||
this.title = title;
|
||||
this.tiles = tiles;
|
||||
}
|
||||
|
||||
public String getTitle() { return title; }
|
||||
public List<Tile> getTiles() { return tiles; }
|
||||
}
|
||||
|
||||
public static class Tile {
|
||||
private final Long questionId;
|
||||
private final int points;
|
||||
private final String text;
|
||||
private final List<Option> answers;
|
||||
|
||||
public Tile(Long questionId, int points, String text, List<Option> answers) {
|
||||
this.questionId = questionId;
|
||||
this.points = points;
|
||||
this.text = text;
|
||||
this.answers = answers;
|
||||
}
|
||||
|
||||
public Long getQuestionId() { return questionId; }
|
||||
public int getPoints() { return points; }
|
||||
public String getText() { return text; }
|
||||
public List<Option> getAnswers() { return answers; }
|
||||
}
|
||||
|
||||
public static class Option {
|
||||
private final Long id;
|
||||
private final String text;
|
||||
|
||||
public Option(Long id, String text) {
|
||||
this.id = id;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public String getText() { return text; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.example.mindflux.dto;
|
||||
|
||||
/** Ergebnis einer abgeschlossenen TechTacular-Runde. */
|
||||
public class TechtacularResult {
|
||||
private final int score;
|
||||
private final int maxScore;
|
||||
private final int correctCount;
|
||||
private final int totalCount;
|
||||
private final int highscore;
|
||||
private final boolean newHighscore;
|
||||
private final long plays;
|
||||
|
||||
public TechtacularResult(int score, int maxScore, int correctCount, int totalCount,
|
||||
int highscore, boolean newHighscore, long plays) {
|
||||
this.score = score;
|
||||
this.maxScore = maxScore;
|
||||
this.correctCount = correctCount;
|
||||
this.totalCount = totalCount;
|
||||
this.highscore = highscore;
|
||||
this.newHighscore = newHighscore;
|
||||
this.plays = plays;
|
||||
}
|
||||
|
||||
public int getScore() { return score; }
|
||||
public int getMaxScore() { return maxScore; }
|
||||
public int getCorrectCount() { return correctCount; }
|
||||
public int getTotalCount() { return totalCount; }
|
||||
public int getHighscore() { return highscore; }
|
||||
public boolean isNewHighscore() { return newHighscore; }
|
||||
public long getPlays() { return plays; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "achievements")
|
||||
public class Achievement {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String username;
|
||||
private String title;
|
||||
private String description;
|
||||
private String icon; // Dateiname des Icons (z.B. "star.png")
|
||||
private LocalDateTime dateAchieved;
|
||||
|
||||
// 🔥 Neuer Konstruktor für schnelle Dummy-Erstellung (ohne username/dateAchieved)
|
||||
public Achievement(String title, String description, String icon, boolean unlocked) {
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.icon = icon;
|
||||
this.dateAchieved = unlocked ? LocalDateTime.now() : null;
|
||||
}
|
||||
|
||||
// ⚡ Standard-Konstruktor (wichtig für JPA!)
|
||||
public Achievement() {
|
||||
}
|
||||
|
||||
// Getter und Setter
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
|
||||
public String getIcon() { return icon; }
|
||||
public void setIcon(String icon) { this.icon = icon; }
|
||||
|
||||
public LocalDateTime getDateAchieved() { return dateAchieved; }
|
||||
public void setDateAchieved(LocalDateTime dateAchieved) { this.dateAchieved = dateAchieved; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Ein Dev-Lab-Spiel (z.B. "SQL Detective"). Bündelt themengleiche Aufgaben unterschiedlicher
|
||||
* Mechaniken. Inhalte DB-gestützt und im Admin pflegbar; server-autoritativ geprüft.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "devlab_games")
|
||||
public class DevLabGame {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(unique = true)
|
||||
private String slug;
|
||||
|
||||
private String title;
|
||||
|
||||
/** Anzeige-Sprache/Domäne, z.B. "SQL", "CSS", "Git". */
|
||||
@Column(name = "code_language")
|
||||
private String language;
|
||||
|
||||
/** Trainingspfad: "syntax" | "debugging" | "visual". */
|
||||
@Column(name = "lab_path")
|
||||
private String path;
|
||||
|
||||
private String description;
|
||||
|
||||
@Column(name = "sort_order")
|
||||
private int sortOrder;
|
||||
|
||||
private boolean active = true;
|
||||
|
||||
@OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@OrderBy("sortOrder ASC, id ASC")
|
||||
private List<DevLabTask> tasks = new ArrayList<>();
|
||||
|
||||
public DevLabGame() {
|
||||
}
|
||||
|
||||
public DevLabGame(String slug, String title, String language, String path,
|
||||
String description, int sortOrder, boolean active) {
|
||||
this.slug = slug;
|
||||
this.title = title;
|
||||
this.language = language;
|
||||
this.path = path;
|
||||
this.description = description;
|
||||
this.sortOrder = sortOrder;
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
public void addTask(DevLabTask task) {
|
||||
task.setGame(this);
|
||||
this.tasks.add(task);
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getSlug() { return slug; }
|
||||
public void setSlug(String slug) { this.slug = slug; }
|
||||
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
|
||||
public String getLanguage() { return language; }
|
||||
public void setLanguage(String language) { this.language = language; }
|
||||
|
||||
public String getPath() { return path; }
|
||||
public void setPath(String path) { this.path = path; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
|
||||
public int getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(int sortOrder) { this.sortOrder = sortOrder; }
|
||||
|
||||
public boolean isActive() { return active; }
|
||||
public void setActive(boolean active) { this.active = active; }
|
||||
|
||||
public List<DevLabTask> getTasks() { return tasks; }
|
||||
public void setTasks(List<DevLabTask> tasks) { this.tasks = tasks; }
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
/**
|
||||
* Eine Dev-Lab-Aufgabe. Der {@code type} bestimmt die Spielmechanik (cloze, choice, order,
|
||||
* spot, match, regex); die mechanikspezifischen Daten inkl. Lösung stehen als JSON im
|
||||
* {@code payload}. Der {@code payload} verlässt nie unverändert das Backend – der Service baut
|
||||
* daraus eine bereinigte Sicht (ohne Lösung) und prüft serverseitig.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "devlab_tasks")
|
||||
public class DevLabTask {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "game_id")
|
||||
private DevLabGame game;
|
||||
|
||||
/** Mechanik: cloze | choice | order | spot | match | regex. */
|
||||
@Column(name = "task_type")
|
||||
private String type;
|
||||
|
||||
/** Kurzer Aufgabentitel (für Anzeige + Admin-Liste). */
|
||||
private String prompt;
|
||||
|
||||
private int points = 100;
|
||||
|
||||
@Column(name = "sort_order")
|
||||
private int sortOrder;
|
||||
|
||||
/** Vollständige Aufgabenspezifikation als JSON (inkl. Lösung). */
|
||||
private String payload;
|
||||
|
||||
public DevLabTask() {
|
||||
}
|
||||
|
||||
public DevLabTask(String type, String prompt, int points, int sortOrder, String payload) {
|
||||
this.type = type;
|
||||
this.prompt = prompt;
|
||||
this.points = points;
|
||||
this.sortOrder = sortOrder;
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public DevLabGame getGame() { return game; }
|
||||
public void setGame(DevLabGame game) { this.game = game; }
|
||||
|
||||
public String getType() { return type; }
|
||||
public void setType(String type) { this.type = type; }
|
||||
|
||||
public String getPrompt() { return prompt; }
|
||||
public void setPrompt(String prompt) { this.prompt = prompt; }
|
||||
|
||||
public int getPoints() { return points; }
|
||||
public void setPoints(int points) { this.points = points; }
|
||||
|
||||
public int getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(int sortOrder) { this.sortOrder = sortOrder; }
|
||||
|
||||
public String getPayload() { return payload; }
|
||||
public void setPayload(String payload) { this.payload = payload; }
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Ein einzelnes Spielergebnis. Bewusst generisch gehalten, damit alle
|
||||
* Mindflux-Spiele (MindMatch, TechTacular, ...) dieselbe Tabelle nutzen können.
|
||||
* Welches Spiel das Ergebnis erzeugt hat, steht in {@link #game}.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "game_scores")
|
||||
public class GameScore {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String username;
|
||||
|
||||
/** Spiel-Kennung, z.B. "mindmatch" (entspricht der Route im Dashboard). */
|
||||
private String game;
|
||||
|
||||
/** Optionales Deck / Level innerhalb des Spiels, z.B. "netzwerk". */
|
||||
private String deck;
|
||||
|
||||
private int score;
|
||||
|
||||
/** Anzahl der Zug-Versuche (kann je nach Spiel null sein). */
|
||||
private Integer moves;
|
||||
|
||||
@Column(name = "duration_seconds")
|
||||
private Integer durationSeconds;
|
||||
|
||||
@Column(name = "played_at")
|
||||
private LocalDateTime playedAt;
|
||||
|
||||
public GameScore() {
|
||||
}
|
||||
|
||||
public GameScore(String username, String game, String deck, int score,
|
||||
Integer moves, Integer durationSeconds, LocalDateTime playedAt) {
|
||||
this.username = username;
|
||||
this.game = game;
|
||||
this.deck = deck;
|
||||
this.score = score;
|
||||
this.moves = moves;
|
||||
this.durationSeconds = durationSeconds;
|
||||
this.playedAt = playedAt;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
|
||||
public String getGame() { return game; }
|
||||
public void setGame(String game) { this.game = game; }
|
||||
|
||||
public String getDeck() { return deck; }
|
||||
public void setDeck(String deck) { this.deck = deck; }
|
||||
|
||||
public int getScore() { return score; }
|
||||
public void setScore(int score) { this.score = score; }
|
||||
|
||||
public Integer getMoves() { return moves; }
|
||||
public void setMoves(Integer moves) { this.moves = moves; }
|
||||
|
||||
public Integer getDurationSeconds() { return durationSeconds; }
|
||||
public void setDurationSeconds(Integer durationSeconds) { this.durationSeconds = durationSeconds; }
|
||||
|
||||
public LocalDateTime getPlayedAt() { return playedAt; }
|
||||
public void setPlayedAt(LocalDateTime playedAt) { this.playedAt = playedAt; }
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Ein MindMatch-Deck (Kategorie) mit seinen Begriffspaaren. Von Admins pflegbar. */
|
||||
@Entity
|
||||
@Table(name = "mindmatch_decks")
|
||||
public class MindMatchDeck {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(unique = true)
|
||||
private String slug;
|
||||
|
||||
private String title;
|
||||
|
||||
@Column(name = "sort_order")
|
||||
private int sortOrder;
|
||||
|
||||
private boolean active = true;
|
||||
|
||||
@OneToMany(mappedBy = "deck", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@OrderBy("id ASC")
|
||||
private List<MindMatchPair> pairs = new ArrayList<>();
|
||||
|
||||
public MindMatchDeck() {
|
||||
}
|
||||
|
||||
public MindMatchDeck(String slug, String title, int sortOrder, boolean active) {
|
||||
this.slug = slug;
|
||||
this.title = title;
|
||||
this.sortOrder = sortOrder;
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
/** Fügt ein Paar hinzu und hält die Rückreferenz konsistent. */
|
||||
public void addPair(MindMatchPair pair) {
|
||||
pair.setDeck(this);
|
||||
this.pairs.add(pair);
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getSlug() { return slug; }
|
||||
public void setSlug(String slug) { this.slug = slug; }
|
||||
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
|
||||
public int getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(int sortOrder) { this.sortOrder = sortOrder; }
|
||||
|
||||
public boolean isActive() { return active; }
|
||||
public void setActive(boolean active) { this.active = active; }
|
||||
|
||||
public List<MindMatchPair> getPairs() { return pairs; }
|
||||
public void setPairs(List<MindMatchPair> pairs) { this.pairs = pairs; }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
/** Ein Begriffspaar innerhalb eines {@link MindMatchDeck}: Begriff + zugehörige Bedeutung. */
|
||||
@Entity
|
||||
@Table(name = "mindmatch_pairs")
|
||||
public class MindMatchPair {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "deck_id")
|
||||
private MindMatchDeck deck;
|
||||
|
||||
private String term;
|
||||
|
||||
/** Spalte heißt "match_text", da MATCH in SQL ein reserviertes Wort ist. */
|
||||
@Column(name = "match_text")
|
||||
private String matchText;
|
||||
|
||||
public MindMatchPair() {
|
||||
}
|
||||
|
||||
public MindMatchPair(String term, String matchText) {
|
||||
this.term = term;
|
||||
this.matchText = matchText;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public MindMatchDeck getDeck() { return deck; }
|
||||
public void setDeck(MindMatchDeck deck) { this.deck = deck; }
|
||||
|
||||
public String getTerm() { return term; }
|
||||
public void setTerm(String term) { this.term = term; }
|
||||
|
||||
public String getMatchText() { return matchText; }
|
||||
public void setMatchText(String matchText) { this.matchText = matchText; }
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Eine Lücke in einer {@link SyntaxSagaMission}. Die {@link #position} entspricht der
|
||||
* Markernummer {@code {{n}}} im Codetemplate. Eine Lücke kann mehrere gültige Lösungen
|
||||
* haben (z.B. {@code 192.168.0.1} oder {@code localhost}). Die Prüfung passiert
|
||||
* serverseitig über {@link #matches(String)} – das Frontend kennt die Lösung nie.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "syntax_saga_gaps")
|
||||
public class SyntaxSagaGap {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "mission_id")
|
||||
private SyntaxSagaMission mission;
|
||||
|
||||
/** 1-basierte Position, = Markernummer {@code {{n}}} im Codetemplate. */
|
||||
@Column(name = "gap_position")
|
||||
private int position;
|
||||
|
||||
private int points = 100;
|
||||
|
||||
/** Groß-/Kleinschreibung relevant? (true für Java-Bezeichner, false z.B. für SQL-Keywords). */
|
||||
@Column(name = "case_sensitive")
|
||||
private boolean caseSensitive = true;
|
||||
|
||||
private String hint;
|
||||
|
||||
/** Kurzlabel/Platzhalter im Eingabefeld, z.B. "Methodenname". */
|
||||
private String placeholder;
|
||||
|
||||
@OneToMany(mappedBy = "gap", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@OrderBy("sortOrder ASC, id ASC")
|
||||
private List<SyntaxSagaSolution> solutions = new ArrayList<>();
|
||||
|
||||
public SyntaxSagaGap() {
|
||||
}
|
||||
|
||||
public SyntaxSagaGap(int position, int points, boolean caseSensitive, String hint, String placeholder) {
|
||||
this.position = position;
|
||||
this.points = points;
|
||||
this.caseSensitive = caseSensitive;
|
||||
this.hint = hint;
|
||||
this.placeholder = placeholder;
|
||||
}
|
||||
|
||||
public void addSolution(SyntaxSagaSolution solution) {
|
||||
solution.setGap(this);
|
||||
this.solutions.add(solution);
|
||||
}
|
||||
|
||||
/** Normalisiert (trim + interne Whitespace zu einem Space). */
|
||||
private static String normalize(String value) {
|
||||
return value == null ? "" : value.trim().replaceAll("\\s+", " ");
|
||||
}
|
||||
|
||||
/** Prüft eine Eingabe serverseitig gegen alle gültigen Lösungen. */
|
||||
public boolean matches(String typed) {
|
||||
if (typed == null) {
|
||||
return false;
|
||||
}
|
||||
String t = normalize(typed);
|
||||
if (t.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (SyntaxSagaSolution solution : solutions) {
|
||||
String sol = normalize(solution.getText());
|
||||
boolean equal = caseSensitive ? t.equals(sol) : t.equalsIgnoreCase(sol);
|
||||
if (equal) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Kanonische Lösung (für die "Auflösung anzeigen"-Funktion). */
|
||||
public String firstSolution() {
|
||||
return solutions.isEmpty() ? "" : solutions.get(0).getText();
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public SyntaxSagaMission getMission() { return mission; }
|
||||
public void setMission(SyntaxSagaMission mission) { this.mission = mission; }
|
||||
|
||||
public int getPosition() { return position; }
|
||||
public void setPosition(int position) { this.position = position; }
|
||||
|
||||
public int getPoints() { return points; }
|
||||
public void setPoints(int points) { this.points = points; }
|
||||
|
||||
public boolean isCaseSensitive() { return caseSensitive; }
|
||||
public void setCaseSensitive(boolean caseSensitive) { this.caseSensitive = caseSensitive; }
|
||||
|
||||
public String getHint() { return hint; }
|
||||
public void setHint(String hint) { this.hint = hint; }
|
||||
|
||||
public String getPlaceholder() { return placeholder; }
|
||||
public void setPlaceholder(String placeholder) { this.placeholder = placeholder; }
|
||||
|
||||
public List<SyntaxSagaSolution> getSolutions() { return solutions; }
|
||||
public void setSolutions(List<SyntaxSagaSolution> solutions) { this.solutions = solutions; }
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Eine Syntax-Saga-Mission (ein Kapitel): Story + optionaler Dialog + Codeblock mit
|
||||
* Lücken. Im {@code codeTemplate} markieren {@code {{1}}}, {@code {{2}}} … die Lücken
|
||||
* (Nummer = {@link SyntaxSagaGap#getPosition() position}). Gespielt wird je
|
||||
* Schwierigkeitsgrad als eigene lineare Saga.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "syntax_saga_missions")
|
||||
public class SyntaxSagaMission {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(unique = true)
|
||||
private String slug;
|
||||
|
||||
private String title;
|
||||
|
||||
/** "junior" | "professional" | "senior". */
|
||||
private String difficulty;
|
||||
|
||||
/** Erzähltext / Szene. */
|
||||
private String narrative;
|
||||
|
||||
/** Optionales Zitat ("Bitte… repariere mich…"). */
|
||||
private String dialog;
|
||||
|
||||
/** Sprache des Codes für Badge/Darstellung, z.B. "java", "sql", "bash". */
|
||||
@Column(name = "code_language")
|
||||
private String language;
|
||||
|
||||
/** Code mit {@code {{n}}}-Markern an den Lücken. */
|
||||
@Column(name = "code_template")
|
||||
private String codeTemplate;
|
||||
|
||||
/** Fortsetzung nach dem Lösen ("Das Terminal erwacht…"). */
|
||||
@Column(name = "success_text")
|
||||
private String successText;
|
||||
|
||||
@Column(name = "sort_order")
|
||||
private int sortOrder;
|
||||
|
||||
private boolean active = true;
|
||||
|
||||
@OneToMany(mappedBy = "mission", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@OrderBy("position ASC, id ASC")
|
||||
private List<SyntaxSagaGap> gaps = new ArrayList<>();
|
||||
|
||||
public SyntaxSagaMission() {
|
||||
}
|
||||
|
||||
public SyntaxSagaMission(String slug, String title, String difficulty, String narrative,
|
||||
String dialog, String language, String codeTemplate,
|
||||
String successText, int sortOrder, boolean active) {
|
||||
this.slug = slug;
|
||||
this.title = title;
|
||||
this.difficulty = difficulty;
|
||||
this.narrative = narrative;
|
||||
this.dialog = dialog;
|
||||
this.language = language;
|
||||
this.codeTemplate = codeTemplate;
|
||||
this.successText = successText;
|
||||
this.sortOrder = sortOrder;
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
public void addGap(SyntaxSagaGap gap) {
|
||||
gap.setMission(this);
|
||||
this.gaps.add(gap);
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getSlug() { return slug; }
|
||||
public void setSlug(String slug) { this.slug = slug; }
|
||||
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
|
||||
public String getDifficulty() { return difficulty; }
|
||||
public void setDifficulty(String difficulty) { this.difficulty = difficulty; }
|
||||
|
||||
public String getNarrative() { return narrative; }
|
||||
public void setNarrative(String narrative) { this.narrative = narrative; }
|
||||
|
||||
public String getDialog() { return dialog; }
|
||||
public void setDialog(String dialog) { this.dialog = dialog; }
|
||||
|
||||
public String getLanguage() { return language; }
|
||||
public void setLanguage(String language) { this.language = language; }
|
||||
|
||||
public String getCodeTemplate() { return codeTemplate; }
|
||||
public void setCodeTemplate(String codeTemplate) { this.codeTemplate = codeTemplate; }
|
||||
|
||||
public String getSuccessText() { return successText; }
|
||||
public void setSuccessText(String successText) { this.successText = successText; }
|
||||
|
||||
public int getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(int sortOrder) { this.sortOrder = sortOrder; }
|
||||
|
||||
public boolean isActive() { return active; }
|
||||
public void setActive(boolean active) { this.active = active; }
|
||||
|
||||
public List<SyntaxSagaGap> getGaps() { return gaps; }
|
||||
public void setGaps(List<SyntaxSagaGap> gaps) { this.gaps = gaps; }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
/** Eine gültige Antwort zu einer {@link SyntaxSagaGap} (eine Lücke kann mehrere haben). */
|
||||
@Entity
|
||||
@Table(name = "syntax_saga_solutions")
|
||||
public class SyntaxSagaSolution {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "gap_id")
|
||||
private SyntaxSagaGap gap;
|
||||
|
||||
@Column(name = "solution_text")
|
||||
private String text;
|
||||
|
||||
@Column(name = "sort_order")
|
||||
private int sortOrder;
|
||||
|
||||
public SyntaxSagaSolution() {
|
||||
}
|
||||
|
||||
public SyntaxSagaSolution(String text, int sortOrder) {
|
||||
this.text = text;
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public SyntaxSagaGap getGap() { return gap; }
|
||||
public void setGap(SyntaxSagaGap gap) { this.gap = gap; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
||||
public int getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(int sortOrder) { this.sortOrder = sortOrder; }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
/** Eine Antwortmöglichkeit zu einer {@link TechtacularQuestion}. */
|
||||
@Entity
|
||||
@Table(name = "techtacular_answers")
|
||||
public class TechtacularAnswer {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "question_id")
|
||||
private TechtacularQuestion question;
|
||||
|
||||
@Column(name = "answer_text")
|
||||
private String text;
|
||||
|
||||
private boolean correct;
|
||||
|
||||
@Column(name = "sort_order")
|
||||
private int sortOrder;
|
||||
|
||||
public TechtacularAnswer() {
|
||||
}
|
||||
|
||||
public TechtacularAnswer(String text, boolean correct, int sortOrder) {
|
||||
this.text = text;
|
||||
this.correct = correct;
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public TechtacularQuestion getQuestion() { return question; }
|
||||
public void setQuestion(TechtacularQuestion question) { this.question = question; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
||||
public boolean isCorrect() { return correct; }
|
||||
public void setCorrect(boolean correct) { this.correct = correct; }
|
||||
|
||||
public int getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(int sortOrder) { this.sortOrder = sortOrder; }
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Eine TechTacular-Kategorie (Spalte im Jeopardy-Board) mit ihren Fragen. */
|
||||
@Entity
|
||||
@Table(name = "techtacular_categories")
|
||||
public class TechtacularCategory {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(unique = true)
|
||||
private String slug;
|
||||
|
||||
private String title;
|
||||
|
||||
@Column(name = "sort_order")
|
||||
private int sortOrder;
|
||||
|
||||
private boolean active = true;
|
||||
|
||||
@OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@OrderBy("points ASC, id ASC")
|
||||
private List<TechtacularQuestion> questions = new ArrayList<>();
|
||||
|
||||
public TechtacularCategory() {
|
||||
}
|
||||
|
||||
public TechtacularCategory(String slug, String title, int sortOrder, boolean active) {
|
||||
this.slug = slug;
|
||||
this.title = title;
|
||||
this.sortOrder = sortOrder;
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
public void addQuestion(TechtacularQuestion question) {
|
||||
question.setCategory(this);
|
||||
this.questions.add(question);
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getSlug() { return slug; }
|
||||
public void setSlug(String slug) { this.slug = slug; }
|
||||
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
|
||||
public int getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(int sortOrder) { this.sortOrder = sortOrder; }
|
||||
|
||||
public boolean isActive() { return active; }
|
||||
public void setActive(boolean active) { this.active = active; }
|
||||
|
||||
public List<TechtacularQuestion> getQuestions() { return questions; }
|
||||
public void setQuestions(List<TechtacularQuestion> questions) { this.questions = questions; }
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Eine Frage mit ihren Antwortmöglichkeiten (genau eine ist korrekt) und Punktwert. */
|
||||
@Entity
|
||||
@Table(name = "techtacular_questions")
|
||||
public class TechtacularQuestion {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "category_id")
|
||||
private TechtacularCategory category;
|
||||
|
||||
@Column(name = "question_text")
|
||||
private String text;
|
||||
|
||||
private String explanation;
|
||||
|
||||
private int points;
|
||||
|
||||
@OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@OrderBy("sortOrder ASC, id ASC")
|
||||
private List<TechtacularAnswer> answers = new ArrayList<>();
|
||||
|
||||
public TechtacularQuestion() {
|
||||
}
|
||||
|
||||
public TechtacularQuestion(String text, String explanation, int points) {
|
||||
this.text = text;
|
||||
this.explanation = explanation;
|
||||
this.points = points;
|
||||
}
|
||||
|
||||
public void addAnswer(TechtacularAnswer answer) {
|
||||
answer.setQuestion(this);
|
||||
this.answers.add(answer);
|
||||
}
|
||||
|
||||
/** Liefert die als korrekt markierte Antwort (oder null). */
|
||||
public TechtacularAnswer correctAnswer() {
|
||||
return answers.stream().filter(TechtacularAnswer::isCorrect).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public TechtacularCategory getCategory() { return category; }
|
||||
public void setCategory(TechtacularCategory category) { this.category = category; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
||||
public String getExplanation() { return explanation; }
|
||||
public void setExplanation(String explanation) { this.explanation = explanation; }
|
||||
|
||||
public int getPoints() { return points; }
|
||||
public void setPoints(int points) { this.points = points; }
|
||||
|
||||
public List<TechtacularAnswer> getAnswers() { return answers; }
|
||||
public void setAnswers(List<TechtacularAnswer> answers) { this.answers = answers; }
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class User {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String username;
|
||||
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
|
||||
private String password;
|
||||
private String email;
|
||||
private String role;
|
||||
|
||||
@Column(name = "profile_picture")
|
||||
private String profilePicture;
|
||||
|
||||
public User() {}
|
||||
|
||||
public User(String username, String password, String email, String role) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.email = email;
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
// —————————————
|
||||
// Getter / Setter
|
||||
// —————————————
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String u) { this.username = u; }
|
||||
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String p) { this.password = p; }
|
||||
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String e) { this.email = e; }
|
||||
|
||||
public String getRole() { return role; }
|
||||
public void setRole(String r) { this.role = r; }
|
||||
|
||||
public String getProfilePicture() { return profilePicture; }
|
||||
public void setProfilePicture(String pictureUrl) { this.profilePicture = pictureUrl; }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.Achievement;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AchievementRepository extends JpaRepository<Achievement, Long> {
|
||||
List<Achievement> findByUsername(String username);
|
||||
|
||||
/** Alle Achievements eines Spielers löschen (z. B. beim Löschen des Kontos). */
|
||||
void deleteByUsername(String username);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface AdminUserRepository extends JpaRepository<User, Long> {
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.DevLabGame;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface DevLabGameRepository extends JpaRepository<DevLabGame, Long> {
|
||||
|
||||
List<DevLabGame> findAllByActiveTrueOrderBySortOrderAscIdAsc();
|
||||
|
||||
List<DevLabGame> findAllByOrderBySortOrderAscIdAsc();
|
||||
|
||||
Optional<DevLabGame> findBySlug(String slug);
|
||||
|
||||
boolean existsBySlug(String slug);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.DevLabTask;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface DevLabTaskRepository extends JpaRepository<DevLabTask, Long> {
|
||||
|
||||
long countByGameId(Long gameId);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.GameScore;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface GameScoreRepository extends JpaRepository<GameScore, Long> {
|
||||
|
||||
/** Bestes Ergebnis eines Spielers in einem Spiel (Highscore). */
|
||||
Optional<GameScore> findTopByUsernameAndGameOrderByScoreDesc(String username, String game);
|
||||
|
||||
/** Zuletzt gespieltes Ergebnis eines Spielers in einem Spiel. */
|
||||
Optional<GameScore> findTopByUsernameAndGameOrderByPlayedAtDesc(String username, String game);
|
||||
|
||||
/** Wie oft ein Spieler ein Spiel abgeschlossen hat. */
|
||||
long countByUsernameAndGame(String username, String game);
|
||||
|
||||
/** Alle Spielstände eines Spielers löschen (z. B. beim Löschen des Kontos). */
|
||||
void deleteByUsername(String username);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.MindMatchDeck;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface MindMatchDeckRepository extends JpaRepository<MindMatchDeck, Long> {
|
||||
|
||||
List<MindMatchDeck> findAllByActiveTrueOrderBySortOrderAscIdAsc();
|
||||
|
||||
List<MindMatchDeck> findAllByOrderBySortOrderAscIdAsc();
|
||||
|
||||
Optional<MindMatchDeck> findBySlug(String slug);
|
||||
|
||||
boolean existsBySlug(String slug);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.MindMatchPair;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface MindMatchPairRepository extends JpaRepository<MindMatchPair, Long> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.SyntaxSagaGap;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface SyntaxSagaGapRepository extends JpaRepository<SyntaxSagaGap, Long> {
|
||||
|
||||
long countByMissionId(Long missionId);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.SyntaxSagaMission;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SyntaxSagaMissionRepository extends JpaRepository<SyntaxSagaMission, Long> {
|
||||
|
||||
/** Aktive Missionen einer Schwierigkeitsstufe in Spielreihenfolge. */
|
||||
List<SyntaxSagaMission> findAllByActiveTrueAndDifficultyOrderBySortOrderAscIdAsc(String difficulty);
|
||||
|
||||
/** Alle aktiven Missionen in Spielreihenfolge (stufenübergreifend). */
|
||||
List<SyntaxSagaMission> findAllByActiveTrueOrderBySortOrderAscIdAsc();
|
||||
|
||||
/** Alle Missionen (Admin-Übersicht). */
|
||||
List<SyntaxSagaMission> findAllByOrderBySortOrderAscIdAsc();
|
||||
|
||||
boolean existsBySlug(String slug);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.SyntaxSagaSolution;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface SyntaxSagaSolutionRepository extends JpaRepository<SyntaxSagaSolution, Long> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.TechtacularAnswer;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface TechtacularAnswerRepository extends JpaRepository<TechtacularAnswer, Long> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.TechtacularCategory;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TechtacularCategoryRepository extends JpaRepository<TechtacularCategory, Long> {
|
||||
|
||||
List<TechtacularCategory> findAllByActiveTrueOrderBySortOrderAscIdAsc();
|
||||
|
||||
List<TechtacularCategory> findAllByOrderBySortOrderAscIdAsc();
|
||||
|
||||
boolean existsBySlug(String slug);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.TechtacularQuestion;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface TechtacularQuestionRepository extends JpaRepository<TechtacularQuestion, Long> {
|
||||
|
||||
long countByCategoryId(Long categoryId);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.example.mindflux.repository;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
// Benutzer anhand des Benutzernamens suchen
|
||||
Optional<User> findByUsername(String username);
|
||||
|
||||
// Benutzer anhand des Benutzernamens löschen
|
||||
void deleteByUsername(String username);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.model.Achievement;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AchievementService {
|
||||
List<Achievement> getAchievementsForUser(String username);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.model.Achievement;
|
||||
import com.example.mindflux.repository.AchievementRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class AchievementServiceImpl implements AchievementService {
|
||||
|
||||
@Autowired
|
||||
private AchievementRepository achievementRepository;
|
||||
|
||||
@Override
|
||||
public List<Achievement> getAchievementsForUser(String username) {
|
||||
return achievementRepository.findByUsername(username);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.model.User;
|
||||
import com.example.mindflux.repository.AdminUserRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class AdminUserService {
|
||||
|
||||
@Autowired
|
||||
private AdminUserRepository userRepository;
|
||||
|
||||
public List<User> findAll() {
|
||||
return userRepository.findAll();
|
||||
}
|
||||
|
||||
public Optional<User> findById(Long id) {
|
||||
return userRepository.findById(id);
|
||||
}
|
||||
|
||||
public User save(User user) {
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
public void deleteById(Long id) {
|
||||
userRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,555 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.dto.DevLabFinishRequest;
|
||||
import com.example.mindflux.dto.DevLabGameRow;
|
||||
import com.example.mindflux.dto.DevLabResult;
|
||||
import com.example.mindflux.dto.ScoreResult;
|
||||
import com.example.mindflux.model.DevLabGame;
|
||||
import com.example.mindflux.model.DevLabTask;
|
||||
import com.example.mindflux.repository.DevLabGameRepository;
|
||||
import com.example.mindflux.repository.DevLabTaskRepository;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Server-autoritative Logik fürs Dev Lab. Aus dem JSON-Payload jeder Aufgabe wird eine
|
||||
* bereinigte Sicht (ohne Lösung) gebaut; geprüft wird ausschließlich serverseitig per
|
||||
* Text-/Reihenfolge-/Verhaltensvergleich gegen den gespeicherten Payload. Mechaniken:
|
||||
* cloze, choice, order, spot, match, regex.
|
||||
*/
|
||||
@Service
|
||||
public class DevLabService {
|
||||
|
||||
private static final String GAME = "devlab";
|
||||
private static final Set<String> KNOWN_TYPES = Set.of("cloze", "choice", "order", "spot", "match", "regex");
|
||||
private static final Set<String> KNOWN_PATHS = Set.of("syntax", "debugging", "visual");
|
||||
|
||||
private final DevLabGameRepository gameRepository;
|
||||
private final DevLabTaskRepository taskRepository;
|
||||
private final GameScoreService gameScoreService;
|
||||
private final ObjectMapper mapper;
|
||||
private final Random random = new Random();
|
||||
|
||||
public DevLabService(DevLabGameRepository gameRepository, DevLabTaskRepository taskRepository,
|
||||
GameScoreService gameScoreService, ObjectMapper mapper) {
|
||||
this.gameRepository = gameRepository;
|
||||
this.taskRepository = taskRepository;
|
||||
this.gameScoreService = gameScoreService;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
// ---------- Katalog (Hub) ----------
|
||||
|
||||
/** Aktive Spiele nach Trainingspfad gruppiert (für den Hub). */
|
||||
@Transactional(readOnly = true)
|
||||
public JsonNode buildCatalog() {
|
||||
ObjectNode root = mapper.createObjectNode();
|
||||
ArrayNode games = root.putArray("games");
|
||||
for (DevLabGame game : gameRepository.findAllByActiveTrueOrderBySortOrderAscIdAsc()) {
|
||||
if (game.getTasks().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
ObjectNode g = games.addObject();
|
||||
g.put("slug", game.getSlug());
|
||||
g.put("title", game.getTitle());
|
||||
g.put("language", game.getLanguage());
|
||||
g.put("path", game.getPath());
|
||||
g.put("description", game.getDescription());
|
||||
g.put("taskCount", game.getTasks().size());
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
// ---------- Spielrunde ----------
|
||||
|
||||
/** Eine Spielrunde: Spiel-Meta + alle aktiven Aufgaben als bereinigte Sicht (ohne Lösung). */
|
||||
@Transactional(readOnly = true)
|
||||
public JsonNode buildSession(String slug) {
|
||||
DevLabGame game = gameRepository.findBySlug(slug).filter(DevLabGame::isActive).orElse(null);
|
||||
ObjectNode root = mapper.createObjectNode();
|
||||
if (game == null) {
|
||||
root.put("found", false);
|
||||
return root;
|
||||
}
|
||||
root.put("found", true);
|
||||
root.put("slug", game.getSlug());
|
||||
root.put("title", game.getTitle());
|
||||
root.put("language", game.getLanguage());
|
||||
root.put("path", game.getPath());
|
||||
root.put("description", game.getDescription());
|
||||
|
||||
ArrayNode tasks = root.putArray("tasks");
|
||||
int maxScore = 0;
|
||||
for (DevLabTask task : game.getTasks()) {
|
||||
ObjectNode view = sanitize(task);
|
||||
if (view == null) {
|
||||
continue;
|
||||
}
|
||||
tasks.add(view);
|
||||
maxScore += task.getPoints();
|
||||
}
|
||||
root.put("maxScore", maxScore);
|
||||
root.put("totalTasks", tasks.size());
|
||||
return root;
|
||||
}
|
||||
|
||||
/** Baut die spielerseitige Sicht einer Aufgabe – ohne Lösung. */
|
||||
private ObjectNode sanitize(DevLabTask task) {
|
||||
JsonNode p = parse(task.getPayload());
|
||||
if (p == null) {
|
||||
return null;
|
||||
}
|
||||
ObjectNode v = mapper.createObjectNode();
|
||||
v.put("taskId", task.getId());
|
||||
v.put("type", task.getType());
|
||||
v.put("prompt", task.getPrompt());
|
||||
v.put("points", task.getPoints());
|
||||
copyText(p, v, "narrative");
|
||||
copyText(p, v, "instruction");
|
||||
copyText(p, v, "question");
|
||||
copyText(p, v, "code");
|
||||
copyText(p, v, "hint");
|
||||
copyText(p, v, "successText");
|
||||
|
||||
switch (task.getType()) {
|
||||
case "cloze" -> {
|
||||
copyText(p, v, "codeTemplate");
|
||||
ArrayNode gaps = v.putArray("gaps");
|
||||
for (JsonNode g : array(p, "gaps")) {
|
||||
ObjectNode gv = gaps.addObject();
|
||||
gv.put("position", g.path("position").asInt());
|
||||
gv.put("placeholder", g.path("placeholder").asText(""));
|
||||
gv.put("hint", g.path("hint").asText(""));
|
||||
}
|
||||
}
|
||||
case "choice" -> v.set("options", shuffledTextArray(stringList(p, "options")));
|
||||
case "order" -> v.set("items", shuffledTextArray(stringList(p, "items")));
|
||||
case "spot" -> {
|
||||
ArrayNode tokens = v.putArray("tokens");
|
||||
for (JsonNode t : array(p, "tokens")) {
|
||||
tokens.add(t.asText(""));
|
||||
}
|
||||
}
|
||||
case "match" -> {
|
||||
ArrayNode left = v.putArray("left");
|
||||
ArrayNode right = v.putArray("right");
|
||||
List<String> rights = new ArrayList<>();
|
||||
for (JsonNode pair : array(p, "pairs")) {
|
||||
if (pair.isArray() && pair.size() >= 2) {
|
||||
left.add(pair.get(0).asText(""));
|
||||
rights.add(pair.get(1).asText(""));
|
||||
}
|
||||
}
|
||||
Collections.shuffle(rights, random);
|
||||
rights.forEach(right::add);
|
||||
}
|
||||
case "regex" -> {
|
||||
ArrayNode samples = v.putArray("samples");
|
||||
for (JsonNode s : array(p, "samples")) {
|
||||
ObjectNode sv = samples.addObject();
|
||||
sv.put("text", s.path("text").asText(""));
|
||||
sv.put("shouldMatch", s.path("shouldMatch").asBoolean(false));
|
||||
}
|
||||
}
|
||||
default -> { return null; }
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// ---------- Prüfung ----------
|
||||
|
||||
/** Prüft eine Aufgabe serverseitig; mit {@code reveal} kommt die Auflösung dazu. */
|
||||
@Transactional(readOnly = true)
|
||||
public JsonNode check(Long taskId, boolean reveal, JsonNode submission) {
|
||||
ObjectNode result = mapper.createObjectNode();
|
||||
DevLabTask task = taskId == null ? null : taskRepository.findById(taskId).orElse(null);
|
||||
JsonNode p = task == null ? null : parse(task.getPayload());
|
||||
if (p == null) {
|
||||
result.put("correct", false);
|
||||
return result;
|
||||
}
|
||||
JsonNode sub = submission == null ? mapper.createObjectNode() : submission;
|
||||
boolean correct = switch (task.getType()) {
|
||||
case "cloze" -> checkCloze(p, sub, result, reveal);
|
||||
case "choice" -> checkChoice(p, sub, result, reveal);
|
||||
case "order" -> checkOrder(p, sub, result, reveal);
|
||||
case "spot" -> checkSpot(p, sub, result, reveal);
|
||||
case "match" -> checkMatch(p, sub, result, reveal);
|
||||
case "regex" -> checkRegex(p, sub, result, reveal);
|
||||
default -> false;
|
||||
};
|
||||
result.put("correct", correct);
|
||||
if (correct && p.hasNonNull("successText")) {
|
||||
result.put("successText", p.get("successText").asText());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean checkCloze(JsonNode p, JsonNode sub, ObjectNode result, boolean reveal) {
|
||||
Map<Integer, String> typed = new HashMap<>();
|
||||
for (JsonNode g : array(sub, "gaps")) {
|
||||
typed.put(g.path("position").asInt(), g.path("text").asText(""));
|
||||
}
|
||||
ArrayNode detail = result.putArray("detail");
|
||||
boolean all = true;
|
||||
for (JsonNode gap : array(p, "gaps")) {
|
||||
int pos = gap.path("position").asInt();
|
||||
boolean caseSensitive = gap.path("caseSensitive").asBoolean(true);
|
||||
boolean ok = anyEquals(stringList(gap, "solutions"), typed.get(pos), caseSensitive);
|
||||
if (!ok) {
|
||||
all = false;
|
||||
}
|
||||
ObjectNode d = detail.addObject();
|
||||
d.put("position", pos);
|
||||
d.put("correct", ok);
|
||||
if (reveal && !ok) {
|
||||
List<String> sols = stringList(gap, "solutions");
|
||||
d.put("solution", sols.isEmpty() ? "" : sols.get(0));
|
||||
}
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
private boolean checkChoice(JsonNode p, JsonNode sub, ObjectNode result, boolean reveal) {
|
||||
List<String> options = stringList(p, "options");
|
||||
int correctIndex = p.path("correctIndex").asInt(-1);
|
||||
String correctText = (correctIndex >= 0 && correctIndex < options.size()) ? options.get(correctIndex) : null;
|
||||
boolean ok = correctText != null && textEquals(correctText, sub.path("choice").asText(""), false);
|
||||
if (reveal && !ok && correctText != null) {
|
||||
result.put("solution", correctText);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
private boolean checkOrder(JsonNode p, JsonNode sub, ObjectNode result, boolean reveal) {
|
||||
List<String> correctOrder = stringList(p, "items");
|
||||
List<String> given = stringList(sub, "order");
|
||||
boolean ok = given.size() == correctOrder.size();
|
||||
for (int i = 0; ok && i < correctOrder.size(); i++) {
|
||||
if (!textEquals(correctOrder.get(i), given.get(i), false)) {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (reveal && !ok) {
|
||||
result.put("solution", String.join(" → ", correctOrder));
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
private boolean checkSpot(JsonNode p, JsonNode sub, ObjectNode result, boolean reveal) {
|
||||
int bugIndex = p.path("bugIndex").asInt(-1);
|
||||
boolean ok = sub.path("index").asInt(-2) == bugIndex;
|
||||
if (reveal && !ok) {
|
||||
ObjectNode sol = result.putObject("solution");
|
||||
sol.put("index", bugIndex);
|
||||
sol.put("fix", p.path("fix").asText(""));
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
private boolean checkMatch(JsonNode p, JsonNode sub, ObjectNode result, boolean reveal) {
|
||||
Map<String, String> correctPairs = new HashMap<>();
|
||||
ArrayNode solution = mapper.createArrayNode();
|
||||
for (JsonNode pair : array(p, "pairs")) {
|
||||
if (pair.isArray() && pair.size() >= 2) {
|
||||
correctPairs.put(normalize(pair.get(0).asText("")), normalize(pair.get(1).asText("")));
|
||||
ArrayNode pv = solution.addArray();
|
||||
pv.add(pair.get(0).asText(""));
|
||||
pv.add(pair.get(1).asText(""));
|
||||
}
|
||||
}
|
||||
int matched = 0;
|
||||
for (JsonNode pair : array(sub, "pairs")) {
|
||||
if (pair.isArray() && pair.size() >= 2) {
|
||||
String want = correctPairs.get(normalize(pair.get(0).asText("")));
|
||||
if (want != null && want.equals(normalize(pair.get(1).asText("")))) {
|
||||
matched++;
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean ok = matched == correctPairs.size() && matched > 0;
|
||||
if (reveal && !ok) {
|
||||
result.set("solution", solution);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
private boolean checkRegex(JsonNode p, JsonNode sub, ObjectNode result, boolean reveal) {
|
||||
String patternText = sub.path("pattern").asText("");
|
||||
boolean ok = false;
|
||||
if (!patternText.isBlank()) {
|
||||
try {
|
||||
Pattern pattern = Pattern.compile(patternText);
|
||||
ok = true;
|
||||
for (JsonNode s : array(p, "samples")) {
|
||||
boolean shouldMatch = s.path("shouldMatch").asBoolean(false);
|
||||
boolean matches = pattern.matcher(s.path("text").asText("")).matches();
|
||||
if (matches != shouldMatch) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
result.put("error", "Ungültiges Regex-Muster.");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (reveal && !ok && p.hasNonNull("pattern")) {
|
||||
result.put("solution", p.get("pattern").asText());
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ---------- Abschluss ----------
|
||||
|
||||
/** Schließt einen Durchlauf ab: Score wird aus den Eingaben recomputed und gespeichert. */
|
||||
@Transactional
|
||||
public DevLabResult finish(String username, DevLabFinishRequest request) {
|
||||
int score = 0;
|
||||
int maxScore = 0;
|
||||
int solved = 0;
|
||||
int total = 0;
|
||||
|
||||
if (request.getItems() != null) {
|
||||
for (DevLabFinishRequest.Item item : request.getItems()) {
|
||||
if (item.getTaskId() == null) {
|
||||
continue;
|
||||
}
|
||||
DevLabTask task = taskRepository.findById(item.getTaskId()).orElse(null);
|
||||
if (task == null) {
|
||||
continue;
|
||||
}
|
||||
total++;
|
||||
maxScore += task.getPoints();
|
||||
JsonNode verdict = check(task.getId(), false, item.getSubmission());
|
||||
if (verdict.path("correct").asBoolean(false) && !item.isRevealed()) {
|
||||
solved++;
|
||||
score += awardedPoints(task.getPoints(), item.getAttempts());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int stars = stars(score, maxScore);
|
||||
int seconds = Math.max(request.getDurationSeconds(), 0);
|
||||
String deck = request.getSlug() == null ? null : request.getSlug();
|
||||
ScoreResult saved = gameScoreService.recordScore(username, GAME, deck, score, solved, seconds);
|
||||
return new DevLabResult(score, maxScore, solved, total, stars,
|
||||
saved.getHighscore(), saved.isNewHighscore(), saved.getPlays());
|
||||
}
|
||||
|
||||
private int awardedPoints(int points, int attempts) {
|
||||
int a = Math.max(1, attempts);
|
||||
double factor = switch (a) {
|
||||
case 1 -> 1.0;
|
||||
case 2 -> 0.6;
|
||||
default -> 0.3;
|
||||
};
|
||||
return (int) Math.round(points * factor);
|
||||
}
|
||||
|
||||
private int stars(int score, int maxScore) {
|
||||
if (maxScore <= 0 || score <= 0) {
|
||||
return 0;
|
||||
}
|
||||
double ratio = (double) score / maxScore;
|
||||
if (ratio >= 0.8) {
|
||||
return 3;
|
||||
}
|
||||
if (ratio >= 0.5) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------- Admin ----------
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<DevLabGameRow> gameRows() {
|
||||
return gameRepository.findAllByOrderBySortOrderAscIdAsc().stream()
|
||||
.map(g -> new DevLabGameRow(g.getId(), g.getSlug(), g.getTitle(), g.getLanguage(),
|
||||
g.getPath(), g.isActive(), taskRepository.countByGameId(g.getId())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public DevLabGame gameWithTasks(Long id) {
|
||||
DevLabGame game = gameRepository.findById(id).orElse(null);
|
||||
if (game != null) {
|
||||
game.getTasks().size(); // lazy init
|
||||
}
|
||||
return game;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Long createGame(String title, String language, String path, String description) {
|
||||
if (title == null || title.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String clean = title.trim();
|
||||
int order = (int) gameRepository.count() + 1;
|
||||
DevLabGame game = new DevLabGame(uniqueSlug(clean), clean, blankToNull(language),
|
||||
normalizePath(path), blankToNull(description), order, true);
|
||||
gameRepository.save(game);
|
||||
return game.getId();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void addTask(Long gameId, String type, String prompt, int points, String payload) {
|
||||
if (type == null || prompt == null || prompt.isBlank()
|
||||
|| !KNOWN_TYPES.contains(type.trim()) || !isValidJson(payload)) {
|
||||
return;
|
||||
}
|
||||
gameRepository.findById(gameId).ifPresent(game -> {
|
||||
int sort = game.getTasks().size();
|
||||
game.addTask(new DevLabTask(type.trim(), cap(prompt.trim(), 300), normalizePoints(points), sort, payload.trim()));
|
||||
gameRepository.save(game);
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateTask(Long taskId, String prompt, int points, String payload) {
|
||||
taskRepository.findById(taskId).ifPresent(task -> {
|
||||
if (prompt != null && !prompt.isBlank()) {
|
||||
task.setPrompt(cap(prompt.trim(), 300));
|
||||
}
|
||||
task.setPoints(normalizePoints(points));
|
||||
if (isValidJson(payload)) {
|
||||
task.setPayload(payload.trim());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteTask(Long taskId) {
|
||||
taskRepository.deleteById(taskId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteGame(Long gameId) {
|
||||
gameRepository.deleteById(gameId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void toggleGame(Long gameId) {
|
||||
gameRepository.findById(gameId).ifPresent(g -> g.setActive(!g.isActive()));
|
||||
}
|
||||
|
||||
// ---------- Helfer ----------
|
||||
|
||||
private boolean isValidJson(String json) {
|
||||
if (json == null || json.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return mapper.readTree(json).isObject();
|
||||
} catch (Exception ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String blankToNull(String value) {
|
||||
return (value == null || value.isBlank()) ? null : value.trim();
|
||||
}
|
||||
|
||||
private String normalizePath(String path) {
|
||||
if (path == null) {
|
||||
return "syntax";
|
||||
}
|
||||
String p = path.trim().toLowerCase();
|
||||
return KNOWN_PATHS.contains(p) ? p : "syntax";
|
||||
}
|
||||
|
||||
private int normalizePoints(int points) {
|
||||
return points <= 0 ? 100 : Math.min(1000, points);
|
||||
}
|
||||
|
||||
private String cap(String value, int max) {
|
||||
return value.length() <= max ? value : value.substring(0, max);
|
||||
}
|
||||
|
||||
private String uniqueSlug(String title) {
|
||||
String base = title.toLowerCase()
|
||||
.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
|
||||
.replaceAll("[^a-z0-9]+", "-")
|
||||
.replaceAll("(^-|-$)", "");
|
||||
if (base.isBlank()) {
|
||||
base = "game";
|
||||
}
|
||||
String slug = base;
|
||||
int counter = 2;
|
||||
while (gameRepository.existsBySlug(slug)) {
|
||||
slug = base + "-" + counter++;
|
||||
}
|
||||
return slug;
|
||||
}
|
||||
|
||||
private JsonNode parse(String json) {
|
||||
if (json == null || json.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return mapper.readTree(json);
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalize(String value) {
|
||||
return value == null ? "" : value.trim().replaceAll("\\s+", " ");
|
||||
}
|
||||
|
||||
private static boolean textEquals(String a, String b, boolean caseSensitive) {
|
||||
String na = normalize(a);
|
||||
String nb = normalize(b);
|
||||
return caseSensitive ? na.equals(nb) : na.equalsIgnoreCase(nb);
|
||||
}
|
||||
|
||||
private static boolean anyEquals(List<String> candidates, String value, boolean caseSensitive) {
|
||||
if (value == null || normalize(value).isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (String candidate : candidates) {
|
||||
if (textEquals(candidate, value, caseSensitive)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Iterable<JsonNode> array(JsonNode node, String field) {
|
||||
JsonNode arr = node == null ? null : node.get(field);
|
||||
return (arr != null && arr.isArray()) ? arr : mapper.createArrayNode();
|
||||
}
|
||||
|
||||
private List<String> stringList(JsonNode node, String field) {
|
||||
List<String> out = new ArrayList<>();
|
||||
for (JsonNode item : array(node, field)) {
|
||||
out.add(item.asText(""));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private ArrayNode shuffledTextArray(List<String> texts) {
|
||||
List<String> copy = new ArrayList<>(texts);
|
||||
Collections.shuffle(copy, random);
|
||||
ArrayNode arr = mapper.createArrayNode();
|
||||
copy.forEach(arr::add);
|
||||
return arr;
|
||||
}
|
||||
|
||||
private void copyText(JsonNode source, ObjectNode target, String field) {
|
||||
if (source.hasNonNull(field)) {
|
||||
String value = source.get(field).asText();
|
||||
if (!value.isEmpty()) {
|
||||
target.put(field, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.dto.GameStats;
|
||||
import com.example.mindflux.dto.ScoreResult;
|
||||
import com.example.mindflux.dto.ScoreSubmission;
|
||||
import com.example.mindflux.model.GameScore;
|
||||
import com.example.mindflux.repository.GameScoreRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Speichert Spielergebnisse und liefert Statistiken. Der Score wird hier
|
||||
* autoritativ berechnet – der Client schickt nur die Rohwerte.
|
||||
*/
|
||||
@Service
|
||||
public class GameScoreService {
|
||||
|
||||
private final GameScoreRepository repository;
|
||||
|
||||
public GameScoreService(GameScoreRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Punktezahl aus Genauigkeit und Tempo.
|
||||
* <ul>
|
||||
* <li>Genauigkeit: {@code 1000 * Paare / Züge} – 1000 Punkte bei perfektem Spiel
|
||||
* (jeder Aufdeck-Versuch ein Treffer), weniger bei vielen Fehlversuchen.</li>
|
||||
* <li>Zeitbonus: bis zu 500 Punkte, der nach 100 Sekunden auf 0 fällt.</li>
|
||||
* </ul>
|
||||
* Identisch in {@code mindmatch.js} gespiegelt, damit der Client sofort denselben
|
||||
* Wert anzeigt.
|
||||
*/
|
||||
public static int calculateScore(int pairs, int moves, int durationSeconds) {
|
||||
if (pairs <= 0) {
|
||||
return 0;
|
||||
}
|
||||
int safeMoves = Math.max(moves, pairs); // niemals weniger Züge als Paare
|
||||
int safeSeconds = Math.max(durationSeconds, 0);
|
||||
|
||||
int accuracyScore = Math.round(1000f * pairs / safeMoves);
|
||||
int timeBonus = Math.max(0, 500 - safeSeconds * 5);
|
||||
return accuracyScore + timeBonus;
|
||||
}
|
||||
|
||||
/** Speichert ein abgeschlossenes MindMatch-Spiel (Score aus Rohwerten berechnet). */
|
||||
public ScoreResult record(String username, String game, ScoreSubmission submission) {
|
||||
int moves = Math.max(submission.getMoves(), submission.getPairs());
|
||||
int seconds = Math.max(submission.getDurationSeconds(), 0);
|
||||
int score = calculateScore(submission.getPairs(), moves, seconds);
|
||||
return recordScore(username, game, submission.getDeck(), score, moves, seconds);
|
||||
}
|
||||
|
||||
/** Speichert ein bereits berechnetes Ergebnis (z.B. TechTacular) und liefert den Highscore-Stand. */
|
||||
public ScoreResult recordScore(String username, String game, String deck,
|
||||
int score, Integer moves, Integer durationSeconds) {
|
||||
GameScore previousBest = repository
|
||||
.findTopByUsernameAndGameOrderByScoreDesc(username, game)
|
||||
.orElse(null);
|
||||
|
||||
repository.save(new GameScore(
|
||||
username, game, deck, score, moves, durationSeconds, LocalDateTime.now()));
|
||||
|
||||
boolean newHighscore = previousBest == null || score > previousBest.getScore();
|
||||
int highscore = previousBest == null ? score : Math.max(previousBest.getScore(), score);
|
||||
long plays = repository.countByUsernameAndGame(username, game);
|
||||
|
||||
return new ScoreResult(score, highscore, newHighscore, plays);
|
||||
}
|
||||
|
||||
/** Aktuelle Statistik (Highscore, Anzahl Spiele, zuletzt gespielt) für ein Spiel. */
|
||||
public GameStats getStats(String username, String game) {
|
||||
int highscore = repository.findTopByUsernameAndGameOrderByScoreDesc(username, game)
|
||||
.map(GameScore::getScore)
|
||||
.orElse(0);
|
||||
long plays = repository.countByUsernameAndGame(username, game);
|
||||
var lastPlayed = repository.findTopByUsernameAndGameOrderByPlayedAtDesc(username, game)
|
||||
.map(score -> score.getPlayedAt().toLocalDate())
|
||||
.orElse(null);
|
||||
return new GameStats(highscore, plays, lastPlayed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.dto.MindMatchDeckView;
|
||||
import com.example.mindflux.dto.MindMatchPairView;
|
||||
import com.example.mindflux.model.MindMatchDeck;
|
||||
import com.example.mindflux.model.MindMatchPair;
|
||||
import com.example.mindflux.repository.MindMatchDeckRepository;
|
||||
import com.example.mindflux.repository.MindMatchPairRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class MindMatchService {
|
||||
|
||||
private final MindMatchDeckRepository deckRepository;
|
||||
private final MindMatchPairRepository pairRepository;
|
||||
|
||||
public MindMatchService(MindMatchDeckRepository deckRepository, MindMatchPairRepository pairRepository) {
|
||||
this.deckRepository = deckRepository;
|
||||
this.pairRepository = pairRepository;
|
||||
}
|
||||
|
||||
// ---------- Spieler ----------
|
||||
|
||||
/** Aktive Decks mit Paaren fürs Spiel (open-in-view=false -> hier initialisiert). */
|
||||
@Transactional(readOnly = true)
|
||||
public List<MindMatchDeckView> activeDecks() {
|
||||
return deckRepository.findAllByActiveTrueOrderBySortOrderAscIdAsc().stream()
|
||||
.map(deck -> new MindMatchDeckView(
|
||||
deck.getSlug(),
|
||||
deck.getTitle(),
|
||||
deck.getPairs().stream()
|
||||
.map(pair -> new MindMatchPairView(pair.getTerm(), pair.getMatchText()))
|
||||
.toList()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// ---------- Admin ----------
|
||||
|
||||
/** Alle Decks (auch inaktive) inkl. Paaren für die Verwaltung. */
|
||||
@Transactional(readOnly = true)
|
||||
public List<MindMatchDeck> allDecksWithPairs() {
|
||||
List<MindMatchDeck> decks = deckRepository.findAllByOrderBySortOrderAscIdAsc();
|
||||
decks.forEach(deck -> deck.getPairs().size()); // Lazy-Collection laden
|
||||
return decks;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void createDeck(String title) {
|
||||
if (title == null || title.isBlank()) {
|
||||
return;
|
||||
}
|
||||
String clean = title.trim();
|
||||
int order = (int) deckRepository.count() + 1;
|
||||
deckRepository.save(new MindMatchDeck(uniqueSlug(clean), clean, order, true));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void addPair(Long deckId, String term, String match) {
|
||||
if (term == null || term.isBlank() || match == null || match.isBlank()) {
|
||||
return;
|
||||
}
|
||||
deckRepository.findById(deckId).ifPresent(deck -> {
|
||||
deck.addPair(new MindMatchPair(term.trim(), match.trim()));
|
||||
deckRepository.save(deck);
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deletePair(Long pairId) {
|
||||
pairRepository.deleteById(pairId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteDeck(Long deckId) {
|
||||
deckRepository.deleteById(deckId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void toggleDeck(Long deckId) {
|
||||
deckRepository.findById(deckId).ifPresent(deck -> deck.setActive(!deck.isActive()));
|
||||
}
|
||||
|
||||
/** Erzeugt aus dem Titel einen URL-tauglichen, eindeutigen Slug. */
|
||||
private String uniqueSlug(String title) {
|
||||
String base = title.toLowerCase()
|
||||
.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
|
||||
.replaceAll("[^a-z0-9]+", "-")
|
||||
.replaceAll("(^-|-$)", "");
|
||||
if (base.isBlank()) {
|
||||
base = "deck";
|
||||
}
|
||||
String slug = base;
|
||||
int counter = 2;
|
||||
while (deckRepository.existsBySlug(slug)) {
|
||||
slug = base + "-" + counter++;
|
||||
}
|
||||
return slug;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user