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>
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
/* 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();
|
||||
})();
|
||||
Reference in New Issue
Block a user