b88962da6f
Dev Lab bündelt mehrere Disziplinen (SQL, Java/Debugging, Git, HTML, CSS, Regex, PHP, REST) in 11 Spielen mit zusammen 189 Aufgaben. Im Gegensatz zur ersten, rein clientseitigen Skizze jetzt server-autoritativ, DB-gestützt und im Admin pflegbar – mit echter Mechanik-Vielfalt statt überall Lückentext: cloze (Lücke tippen), choice (Multiple Choice), order (Bausteine zusammensetzen), spot (Bug anklicken), match (Paare zuordnen), regex (Muster schreiben mit Live-Treffer-Vorschau). Prüfung und Score ausschließlich serverseitig – die Lösungen verlassen nie das Backend; Eingabe-Platzhalter sind neutrale Labels (Lösungs-Leak behoben). Fehler-Mechanik mit Versuchen + Hinweis + Auflösung, kein Game-Over; Score über die generische game_scores-Tabelle (Kennung devlab, deck=Spiel-Slug). - Entities DevLabGame/DevLabTask (JSON-payload) + Flyway V8__devlab.sql - DevLabService mit sechs Mechanik-Checkern; API /api/devlab/** (CSRF-ausgenommen, devlab in KNOWN_GAMES) - Admin /admin/devlab (Spiele + Aufgaben pflegen); polymorphe Frontend-Engine devlabgame.js + DB-getriebener Hub devlab.js - Inhalte aus devlab/content.json (idempotenter Seeder); ersetzt die erste clientseitige Dev-Lab-Skizze (devlab.js/devlabgame.js/-game.html/-game.css neu) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
109 lines
6.6 KiB
HTML
109 lines
6.6 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de" data-theme="dark"
|
||
xmlns:th="http://www.thymeleaf.org"
|
||
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title th:text="${game.title} + ' – Dev Lab'">Dev-Lab-Spiel</title>
|
||
<link rel="stylesheet" href="/css/theme.css">
|
||
<script src="/js/theme.js"></script>
|
||
<style>
|
||
.dlg { max-width: 1000px; margin: 0 auto; padding: 0 1rem; width: 100%; }
|
||
.dlg .grid2 { display: grid; grid-template-columns: 160px 1fr; gap: 0.7rem; }
|
||
.dlg .field { margin-bottom: 0.7rem; display: flex; flex-direction: column; gap: 0.35rem; }
|
||
.dlg .field > label { color: var(--accent); font-weight: 700; font-size: 0.85rem; }
|
||
.dlg textarea.mf-input { font-family: "JetBrains Mono", monospace; resize: vertical; min-height: 8rem; font-size: 0.82rem; }
|
||
.dlg .task { background: var(--surface); border: 1px solid var(--border); border-left: 4px solid var(--secondary); border-radius: 12px; padding: 0.9rem 1.1rem; margin-bottom: 0.7rem; box-shadow: var(--g-soft); }
|
||
.dlg .task-head { display: flex; align-items: center; gap: 0.6rem; justify-content: space-between; flex-wrap: wrap; }
|
||
.dlg .ttype { background: var(--glow-w); color: var(--accent); font-weight: 800; border: 1px solid var(--border-accent); border-radius: 7px; padding: 0.14rem 0.55rem; font-size: 0.78rem; font-family: monospace; }
|
||
.dlg .tprompt { color: var(--heading); font-weight: 600; }
|
||
.dlg .hint { color: var(--muted); font-size: 0.82rem; margin-bottom: 0.8rem; line-height: 1.5; }
|
||
.dlg .hint code { color: var(--accent); }
|
||
.dlg .empty { color: var(--danger); }
|
||
.dlg details { margin-top: 0.5rem; }
|
||
.dlg summary { cursor: pointer; color: var(--accent); font-size: 0.85rem; }
|
||
@media (max-width: 600px) { .dlg .grid2 { grid-template-columns: 1fr; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="mf-theme-float"><button class="mf-theme"></button></div>
|
||
<div class="mf-container" style="max-width:1040px">
|
||
<div class="mf-page-head">
|
||
<a th:href="@{/admin/devlab}" class="mf-back">⟵ Zurück zur Spieleliste</a>
|
||
</div>
|
||
<header style="margin-bottom:1.4rem">
|
||
<div class="mf-eyebrow">Dev Lab · Spiel</div>
|
||
<h1 style="color:var(--heading); font-size:1.75rem; font-weight:800; margin-top:.35rem" th:text="'⚗️ ' + ${game.title}">Spiel</h1>
|
||
<p style="color:var(--muted); margin-top:.3rem">
|
||
<span th:text="'/' + ${game.slug}">/slug</span> ·
|
||
<span th:text="${game.path}">syntax</span> ·
|
||
<span th:text="${game.language}">SQL</span>
|
||
</p>
|
||
</header>
|
||
|
||
<main class="dlg">
|
||
<h4 style="color:var(--accent); margin:0 0 0.7rem; text-transform:uppercase; letter-spacing:.08em; font-size:.8rem">Aufgaben</h4>
|
||
|
||
<div class="task" th:each="t : ${game.tasks}">
|
||
<div class="task-head">
|
||
<span><span class="ttype" th:text="${t.type}">cloze</span> <span class="tprompt" th:text="${t.prompt}">Titel</span></span>
|
||
<form th:action="@{'/admin/devlab/tasks/' + ${t.id} + '/delete'}" method="post" onsubmit="return confirm('Aufgabe löschen?');">
|
||
<input type="hidden" name="gameId" th:value="${game.id}">
|
||
<button type="submit" class="mf-btn danger">✕ Löschen</button>
|
||
</form>
|
||
</div>
|
||
<details>
|
||
<summary>Bearbeiten (JSON-Payload)</summary>
|
||
<form th:action="@{'/admin/devlab/tasks/' + ${t.id} + '/update'}" method="post" style="margin-top:.6rem">
|
||
<input type="hidden" name="gameId" th:value="${game.id}">
|
||
<div class="grid2">
|
||
<div class="field"><label>Punkte</label><input class="mf-input" type="number" name="points" th:value="${t.points}" min="10" max="1000"></div>
|
||
<div class="field"><label>Titel</label><input class="mf-input" type="text" name="prompt" th:value="${t.prompt}" required></div>
|
||
</div>
|
||
<div class="field"><label>Payload (JSON)</label><textarea class="mf-input" name="payload" th:text="${t.payload}" required></textarea></div>
|
||
<button type="submit" class="mf-btn">💾 Speichern</button>
|
||
</form>
|
||
</details>
|
||
</div>
|
||
<p th:if="${game.tasks.isEmpty()}" class="empty">Noch keine Aufgaben – unten hinzufügen. Ohne Aufgaben erscheint das Spiel nicht.</p>
|
||
|
||
<div class="mf-panel accent" style="margin-top:1.6rem">
|
||
<h4>Aufgabe hinzufügen</h4>
|
||
<p class="hint">Typ wählen und den passenden <strong>JSON-Payload</strong> eintragen. Pflichtfelder je Typ:
|
||
<code>cloze</code>: codeTemplate (<code>{{1}}</code>…), gaps[{position, placeholder, hint, caseSensitive, solutions[]}] ·
|
||
<code>choice</code>: question, options[], correctIndex ·
|
||
<code>order</code>: instruction, items[] (richtige Reihenfolge) ·
|
||
<code>spot</code>: tokens[], bugIndex, fix ·
|
||
<code>match</code>: instruction, pairs[[l,r]…] ·
|
||
<code>regex</code>: instruction, samples[{text, shouldMatch}], pattern.
|
||
Optional überall: narrative, hint, successText. Platzhalter nie die Lösung!</p>
|
||
<form th:action="@{'/admin/devlab/games/' + ${game.id} + '/tasks'}" method="post">
|
||
<div class="grid2">
|
||
<div class="field"><label>Typ</label>
|
||
<select class="mf-select" name="type">
|
||
<option value="cloze">cloze – Lücke tippen</option>
|
||
<option value="choice">choice – Multiple Choice</option>
|
||
<option value="order">order – Zusammensetzen</option>
|
||
<option value="spot">spot – Bug finden</option>
|
||
<option value="match">match – Zuordnen</option>
|
||
<option value="regex">regex – Muster schreiben</option>
|
||
</select>
|
||
</div>
|
||
<div class="field"><label>Titel</label><input class="mf-input" type="text" name="prompt" placeholder="kurzer Aufgabentitel" required></div>
|
||
</div>
|
||
<div class="field" style="max-width:160px"><label>Punkte</label><input class="mf-input" type="number" name="points" value="100" min="10" max="1000"></div>
|
||
<div class="field"><label>Payload (JSON)</label><textarea class="mf-input" name="payload" placeholder='{ "question": "...", "options": ["A","B"], "correctIndex": 0, "hint": "...", "successText": "..." }' required></textarea></div>
|
||
<button type="submit" class="mf-btn">➕ Aufgabe speichern</button>
|
||
</form>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
<footer class="mf-footer">
|
||
<a href="#">Support</a><a href="#">Feedback</a>
|
||
<a href="/nutzungsbedingungen">Nutzungsbedingungen</a><a href="/richtlinien">Richtlinien</a>
|
||
<a href="/datenschutz">Datenschutz</a><a href="/impressum">Impressum</a>
|
||
</footer>
|
||
</body>
|
||
</html>
|