9022961762
Ersetzt das Hello-World-Gerüst durch die vollständige Anwendung (com.example.mindflux: 64 Klassen, Thymeleaf-Templates, Static-Assets, Flyway V1–V6). Deploy-Infrastruktur (.gitea, Dockerfile) bleibt erhalten. - pom: volle Abhängigkeiten (Security, Thymeleaf, JPA, Flyway), Spring Boot 3.4.4 / Java 17 - docker-compose: SPRING_DATASOURCE_* unverändert; App lauscht auf 8080 (nginx -> mindflux:8080); Uploads-Volume; Admin-Passwörter via Gitea-Secrets - Workflow: Deploy-Step injiziert MINDFLUX_ADMIN*_PASSWORD aus Secrets - .dockerignore ergänzt; Hello-World (com.mindflux) und Test-Datei 'main' entfernt Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
268 lines
9.0 KiB
JavaScript
268 lines
9.0 KiB
JavaScript
/* TechTacular – IT-Quizduell im Jeopardy-Stil.
|
||
Board kommt aus /api/techtacular/board (ohne Lösung), Prüfung + Score serverseitig. */
|
||
(() => {
|
||
'use strict';
|
||
|
||
const GAME = 'techtacular';
|
||
|
||
const boardEl = document.getElementById('tt-board');
|
||
const newBtn = document.getElementById('tt-new');
|
||
const finishBtn = document.getElementById('tt-finish');
|
||
const cancelBtn = document.getElementById('tt-cancel');
|
||
const scoreEl = document.getElementById('tt-score');
|
||
const answeredEl= document.getElementById('tt-answered');
|
||
const timeEl = document.getElementById('tt-time');
|
||
const bestEl = document.getElementById('tt-best');
|
||
|
||
const qOverlay = document.getElementById('tt-question');
|
||
const qCat = document.getElementById('tt-q-cat');
|
||
const qPoints = document.getElementById('tt-q-points');
|
||
const qText = document.getElementById('tt-q-text');
|
||
const qAnswers = document.getElementById('tt-q-answers');
|
||
const qFeedback = document.getElementById('tt-q-feedback');
|
||
const qNext = document.getElementById('tt-q-next');
|
||
|
||
const resultEl = document.getElementById('tt-result');
|
||
const resultScore = document.getElementById('tt-result-score');
|
||
const resultMax = document.getElementById('tt-result-max');
|
||
const resultDetail= document.getElementById('tt-result-detail');
|
||
const resultHigh = document.getElementById('tt-result-high');
|
||
const againBtn = document.getElementById('tt-again');
|
||
|
||
let board = null;
|
||
let previousIds = []; // Frage-IDs der Vorrunde -> werden im nächsten Board vermieden
|
||
let tiles = []; // Tile-Zustände {data, categoryTitle, answered, correct, el}
|
||
let answeredCount = 0;
|
||
let score = 0;
|
||
let seconds = 0;
|
||
let timerId = null;
|
||
let running = false;
|
||
let current = null; // gerade offene Kachel
|
||
const submitted = []; // {questionId, answerId} für /finish
|
||
|
||
function formatTime(s) {
|
||
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
|
||
}
|
||
|
||
function startTimer() {
|
||
if (timerId) return;
|
||
timerId = setInterval(() => { seconds++; timeEl.textContent = formatTime(seconds); }, 1000);
|
||
}
|
||
function stopTimer() { clearInterval(timerId); timerId = null; }
|
||
|
||
function setIdle(message) {
|
||
boardEl.style.gridTemplateColumns = '';
|
||
boardEl.innerHTML = `<p class="tt-idle">${message}</p>`;
|
||
}
|
||
|
||
function updateHud() {
|
||
scoreEl.textContent = String(score);
|
||
answeredEl.textContent = `${answeredCount} / ${tiles.length}`;
|
||
}
|
||
|
||
function renderBoard() {
|
||
boardEl.innerHTML = '';
|
||
boardEl.style.gridTemplateColumns = `repeat(${board.categories.length}, 1fr)`;
|
||
tiles = [];
|
||
|
||
board.categories.forEach((category) => {
|
||
const col = document.createElement('div');
|
||
col.className = 'tt-col';
|
||
|
||
const title = document.createElement('div');
|
||
title.className = 'tt-cat-title';
|
||
title.textContent = category.title;
|
||
col.appendChild(title);
|
||
|
||
category.tiles.forEach((tileData) => {
|
||
const tileEl = document.createElement('button');
|
||
tileEl.type = 'button';
|
||
tileEl.className = 'tt-tile';
|
||
tileEl.textContent = String(tileData.points);
|
||
|
||
const state = { data: tileData, categoryTitle: category.title, answered: false, correct: false, el: tileEl };
|
||
tileEl.addEventListener('click', () => openTile(state));
|
||
tiles.push(state);
|
||
col.appendChild(tileEl);
|
||
});
|
||
|
||
boardEl.appendChild(col);
|
||
});
|
||
}
|
||
|
||
function openTile(state) {
|
||
if (!running || state.answered || current) return;
|
||
current = state;
|
||
|
||
qCat.textContent = state.categoryTitle;
|
||
qPoints.textContent = String(state.data.points);
|
||
qText.textContent = state.data.text;
|
||
qFeedback.textContent = '';
|
||
qFeedback.className = 'tt-q-feedback';
|
||
qNext.hidden = true;
|
||
|
||
qAnswers.innerHTML = '';
|
||
state.data.answers.forEach((answer) => {
|
||
const btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'tt-answer';
|
||
btn.textContent = `Was ist ${answer.text}?`;
|
||
btn.addEventListener('click', () => chooseAnswer(state, answer.id, btn));
|
||
qAnswers.appendChild(btn);
|
||
});
|
||
|
||
qOverlay.hidden = false;
|
||
}
|
||
|
||
async function chooseAnswer(state, answerId, btn) {
|
||
if (state.answered) return;
|
||
state.answered = true; // sofort sperren (eine Antwort pro Frage)
|
||
|
||
const buttons = Array.from(qAnswers.querySelectorAll('.tt-answer'));
|
||
buttons.forEach((b) => { b.disabled = true; });
|
||
|
||
submitted.push({ questionId: state.data.questionId, answerId });
|
||
|
||
try {
|
||
const res = await fetch('/api/techtacular/answer', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ questionId: state.data.questionId, answerId })
|
||
});
|
||
const data = await res.json();
|
||
|
||
state.correct = data.correct;
|
||
// richtige Antwort grün, eigene falsche rot markieren
|
||
state.data.answers.forEach((answer, i) => {
|
||
if (answer.id === data.correctAnswerId) buttons[i].classList.add('correct');
|
||
});
|
||
if (!data.correct) btn.classList.add('wrong');
|
||
|
||
const correctTerm = (state.data.answers.find((a) => a.id === data.correctAnswerId) || {}).text || '';
|
||
if (data.correct) {
|
||
score += data.points;
|
||
qFeedback.textContent = `Richtig! +${data.points}. ${data.explanation || ''}`.trim();
|
||
qFeedback.className = 'tt-q-feedback correct';
|
||
} else {
|
||
qFeedback.textContent = `Falsch – richtig: „Was ist ${correctTerm}?“. ${data.explanation || ''}`.trim();
|
||
qFeedback.className = 'tt-q-feedback wrong';
|
||
}
|
||
updateHud();
|
||
} catch (err) {
|
||
qFeedback.textContent = 'Antwort konnte nicht geprüft werden.';
|
||
qFeedback.className = 'tt-q-feedback wrong';
|
||
console.error(err);
|
||
}
|
||
|
||
qNext.hidden = false;
|
||
}
|
||
|
||
function closeQuestion() {
|
||
if (!current) return;
|
||
const state = current;
|
||
current = null;
|
||
qOverlay.hidden = true;
|
||
|
||
state.el.classList.add('answered', state.correct ? 'correct' : 'wrong');
|
||
state.el.textContent = state.correct ? `✓ ${state.data.points}` : '✗';
|
||
answeredCount++;
|
||
updateHud();
|
||
|
||
if (answeredCount === tiles.length) {
|
||
finishGame();
|
||
}
|
||
}
|
||
|
||
async function finishGame() {
|
||
if (!running) return;
|
||
running = false;
|
||
stopTimer();
|
||
finishBtn.hidden = true;
|
||
cancelBtn.hidden = true;
|
||
|
||
try {
|
||
const res = await fetch('/api/techtacular/finish', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ durationSeconds: seconds, answers: submitted })
|
||
});
|
||
const data = await res.json();
|
||
resultScore.textContent = String(data.score);
|
||
resultMax.textContent = String(data.maxScore);
|
||
resultDetail.textContent =
|
||
`${data.correctCount} von ${data.totalCount} Fragen richtig · ${formatTime(seconds)}`;
|
||
resultHigh.hidden = !data.newHighscore;
|
||
bestEl.textContent = String(data.highscore);
|
||
resultEl.hidden = false;
|
||
} catch (err) {
|
||
console.error('Runde konnte nicht gespeichert werden:', err);
|
||
}
|
||
}
|
||
|
||
function cancelGame() {
|
||
if (!running) return;
|
||
if (answeredCount > 0 && !window.confirm(
|
||
'Aktuelle Runde wirklich abbrechen? Sie wird nicht gespeichert.')) {
|
||
return;
|
||
}
|
||
running = false;
|
||
current = null;
|
||
stopTimer();
|
||
qOverlay.hidden = true;
|
||
finishBtn.hidden = true;
|
||
cancelBtn.hidden = true;
|
||
setIdle('Runde abgebrochen – klicke „Neues Spiel“ für ein frisches Board.');
|
||
}
|
||
|
||
async function loadStats() {
|
||
try {
|
||
const res = await fetch(`/api/games/${GAME}/stats`);
|
||
if (!res.ok) return;
|
||
const stats = await res.json();
|
||
bestEl.textContent = stats.highscore > 0 ? String(stats.highscore) : '–';
|
||
} catch (err) { /* egal */ }
|
||
}
|
||
|
||
async function newGame() {
|
||
stopTimer();
|
||
seconds = 0; score = 0; answeredCount = 0; current = null;
|
||
submitted.length = 0;
|
||
timeEl.textContent = '0:00';
|
||
resultEl.hidden = true;
|
||
qOverlay.hidden = true;
|
||
|
||
const excludeParam = previousIds.length ? ('?exclude=' + previousIds.join(',')) : '';
|
||
try {
|
||
const res = await fetch('/api/techtacular/board' + excludeParam);
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
board = await res.json();
|
||
} catch (err) {
|
||
console.error('Board konnte nicht geladen werden:', err);
|
||
setIdle('Board konnte nicht geladen werden.');
|
||
return;
|
||
}
|
||
|
||
if (!board.categories || !board.categories.length) {
|
||
setIdle('Noch keine Kategorien vorhanden. Ein Admin kann sie unter „TechTacular verwalten“ anlegen.');
|
||
return;
|
||
}
|
||
|
||
renderBoard();
|
||
previousIds = board.categories.flatMap((c) => c.tiles.map((t) => t.questionId));
|
||
updateHud();
|
||
running = true;
|
||
finishBtn.hidden = false;
|
||
cancelBtn.hidden = false;
|
||
startTimer();
|
||
}
|
||
|
||
newBtn.addEventListener('click', newGame);
|
||
againBtn.addEventListener('click', newGame);
|
||
finishBtn.addEventListener('click', finishGame);
|
||
cancelBtn.addEventListener('click', cancelGame);
|
||
qNext.addEventListener('click', closeQuestion);
|
||
|
||
loadStats();
|
||
newGame();
|
||
})();
|