Files
mindflux/src/main/resources/static/js/techtacular.js
T
AliceDarkest 9022961762 Volle Mindflux-App ins Deploy-Repo übernehmen
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>
2026-06-26 14:51:30 +02:00

268 lines
9.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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();
})();