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:
AliceDarkest
2026-06-26 14:51:30 +02:00
parent f3309c1574
commit 9022961762
185 changed files with 22772 additions and 142 deletions
@@ -0,0 +1,29 @@
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-fluxcheck]').forEach((check) => {
const button = check.querySelector('button');
const result = check.querySelector('.fluxcheck-result');
if (!button || !result) {
return;
}
button.addEventListener('click', () => {
const selected = check.querySelector('input[type="radio"]:checked');
if (!selected) {
result.textContent = 'Wähle zuerst eine Antwort aus.';
result.className = 'fluxcheck-result wrong';
return;
}
const expected = check.dataset.answer;
const explanation = check.dataset.explanation || '';
const isCorrect = selected.value === expected;
result.textContent = isCorrect
? 'Richtig. ' + explanation
: 'Noch nicht. ' + explanation;
result.className = 'fluxcheck-result ' + (isCorrect ? 'correct' : 'wrong');
});
});
});
@@ -0,0 +1,179 @@
document.addEventListener('DOMContentLoaded', async () => {
const script = document.querySelector('script[data-questions-src]');
const testRoot = document.querySelector('[data-finalflux-test]');
if (!script || !testRoot) {
return;
}
const elements = {
questionCounter: testRoot.querySelector('[data-question-counter]'),
scoreCounter: testRoot.querySelector('[data-score-counter]'),
categoryTitle: testRoot.querySelector('[data-category-title]'),
categoryHint: testRoot.querySelector('[data-category-hint]'),
questionText: testRoot.querySelector('[data-question-text]'),
answerOptions: testRoot.querySelector('[data-answer-options]'),
answerFeedback: testRoot.querySelector('[data-answer-feedback]'),
checkAnswer: testRoot.querySelector('[data-check-answer]'),
skipQuestion: testRoot.querySelector('[data-skip-question]'),
nextQuestion: testRoot.querySelector('[data-next-question]'),
testResult: document.querySelector('[data-test-result]'),
resultSummary: document.querySelector('[data-result-summary]'),
categorySummary: document.querySelector('[data-category-summary]'),
restartTest: document.querySelector('[data-restart-test]')
};
let questions = [];
let currentIndex = 0;
let score = 0;
let answered = false;
const categoryStats = new Map();
function setError(message) {
elements.questionText.textContent = message;
elements.answerOptions.innerHTML = '';
elements.checkAnswer.disabled = true;
elements.skipQuestion.disabled = true;
elements.nextQuestion.disabled = true;
}
function ensureCategory(category) {
if (!categoryStats.has(category)) {
categoryStats.set(category, { correct: 0, total: 0, skipped: 0 });
}
return categoryStats.get(category);
}
function renderQuestion() {
const question = questions[currentIndex];
answered = false;
elements.questionCounter.textContent = `Frage ${currentIndex + 1} von ${questions.length}`;
elements.scoreCounter.textContent = `${score} richtig`;
elements.categoryTitle.textContent = question.category;
elements.categoryHint.textContent = question.hint || '';
elements.questionText.textContent = question.text;
elements.answerFeedback.textContent = '';
elements.answerFeedback.className = 'fluxcheck-result';
elements.nextQuestion.disabled = true;
elements.checkAnswer.disabled = false;
elements.skipQuestion.disabled = false;
elements.answerOptions.innerHTML = '';
question.answers.forEach((answer, index) => {
const label = document.createElement('label');
const input = document.createElement('input');
const text = document.createElement('span');
input.type = 'radio';
input.name = 'answer';
input.value = String(index);
text.textContent = answer;
label.append(input, text);
elements.answerOptions.append(label);
});
}
function markAnswered(isCorrect, skipped) {
const question = questions[currentIndex];
const stats = ensureCategory(question.category);
stats.total += 1;
if (skipped) {
stats.skipped += 1;
}
if (isCorrect) {
stats.correct += 1;
score += 1;
}
}
function finishIfDone() {
if (currentIndex < questions.length - 1) {
currentIndex += 1;
renderQuestion();
return;
}
testRoot.hidden = true;
elements.testResult.hidden = false;
elements.resultSummary.textContent = `Du hast ${score} von ${questions.length} Fragen richtig beantwortet.`;
elements.categorySummary.innerHTML = '';
Array.from(categoryStats.entries()).forEach(([category, stats]) => {
const skipped = stats.skipped ? `, ${stats.skipped} übersprungen` : '';
const item = document.createElement('li');
const categoryLabel = document.createElement('strong');
categoryLabel.textContent = `${category}:`;
item.append(categoryLabel, ` ${stats.correct} von ${stats.total} richtig${skipped}`);
elements.categorySummary.append(item);
});
}
elements.checkAnswer.addEventListener('click', () => {
if (answered) {
return;
}
const question = questions[currentIndex];
const selected = elements.answerOptions.querySelector('input[type="radio"]:checked');
if (!selected) {
elements.answerFeedback.textContent = 'Wähle zuerst eine Antwort aus oder überspringe die Frage.';
elements.answerFeedback.className = 'fluxcheck-result wrong';
return;
}
const isCorrect = Number(selected.value) === question.correct;
markAnswered(isCorrect, false);
answered = true;
elements.answerFeedback.textContent = (isCorrect ? 'Richtig. ' : 'Noch nicht. ') + question.explanation;
elements.answerFeedback.className = 'fluxcheck-result ' + (isCorrect ? 'correct' : 'wrong');
elements.scoreCounter.textContent = `${score} richtig`;
elements.nextQuestion.disabled = false;
elements.checkAnswer.disabled = true;
elements.skipQuestion.disabled = true;
});
elements.skipQuestion.addEventListener('click', () => {
if (!answered) {
markAnswered(false, true);
}
finishIfDone();
});
elements.nextQuestion.addEventListener('click', finishIfDone);
elements.restartTest.addEventListener('click', () => {
currentIndex = 0;
score = 0;
categoryStats.clear();
testRoot.hidden = false;
elements.testResult.hidden = true;
renderQuestion();
});
try {
const response = await fetch(script.dataset.questionsSrc);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const payload = await response.json();
questions = Array.isArray(payload) ? payload : payload.questions;
if (!Array.isArray(questions) || questions.length === 0) {
setError('Dieser Fragenkatalog enthält noch keine Fragen.');
return;
}
renderQuestion();
} catch (error) {
console.error(error);
setError('Der Fragenkatalog konnte nicht geladen werden.');
}
});
+306
View File
@@ -0,0 +1,306 @@
/* MindMatch – Lern-Memory (Begriff <-> Bedeutung).
Decks kommen aus der DB-API (/api/mindmatch/decks), Highscore aus /api/games/mindmatch. */
(() => {
'use strict';
const GAME = 'mindmatch';
const board = document.getElementById('mm-board');
const deckSelect = document.getElementById('mm-deck');
const newBtn = document.getElementById('mm-new');
const cancelBtn = document.getElementById('mm-cancel');
const movesEl = document.getElementById('mm-moves');
const timeEl = document.getElementById('mm-time');
const pairsEl = document.getElementById('mm-pairs');
const bestEl = document.getElementById('mm-best');
const resultEl = document.getElementById('mm-result');
const resultStars = document.getElementById('mm-result-stars');
const resultScore = document.getElementById('mm-result-score');
const resultDetail= document.getElementById('mm-result-detail');
const resultHigh = document.getElementById('mm-result-highscore');
const againBtn = document.getElementById('mm-again');
const decksBySlug = new Map(); // slug -> [{term, match}]
// Laufender Spielzustand
let pairs = [];
let totalPairs = 0;
let firstCard = null; // erste aufgedeckte, noch nicht zugeordnete Karte
let lock = false; // Eingabesperre während der Auswertung
let moves = 0;
let matched = 0;
let seconds = 0;
let timerId = null;
let running = false; // läuft gerade eine Runde? (steuert den Abbrechen-Button)
/* Punkteformel – identisch zu GameScoreService.calculateScore (Server bleibt autoritativ). */
function calculateScore(p, mv, secs) {
if (p <= 0) return 0;
const safeMoves = Math.max(mv, p);
const safeSecs = Math.max(secs, 0);
const accuracy = Math.round(1000 * p / safeMoves);
const timeBonus = Math.max(0, 500 - safeSecs * 5);
return accuracy + timeBonus;
}
function starsFor(p, mv) {
if (mv <= Math.ceil(p * 1.4)) return 3;
if (mv <= p * 2) return 2;
return 1;
}
function formatTime(s) {
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
}
function shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
function startTimer() {
if (timerId) return;
timerId = setInterval(() => {
seconds++;
timeEl.textContent = formatTime(seconds);
}, 1000);
}
function stopTimer() {
clearInterval(timerId);
timerId = null;
}
function resetHud() {
seconds = 0; moves = 0; matched = 0;
timeEl.textContent = '0:00';
movesEl.textContent = '0';
pairsEl.textContent = '0 / 0';
}
/* Leeres Brett mit Hinweis; räumt die per-JS gesetzten Grid-Inline-Styles ab. */
function setIdle(message) {
board.style.gridTemplateColumns = '';
board.style.maxWidth = '';
board.innerHTML = '';
const hint = document.createElement('p');
hint.className = 'mm-idle';
hint.textContent = message;
board.appendChild(hint);
}
function buildCards(pairList) {
const cards = [];
pairList.forEach((pair, index) => {
cards.push({ pairId: index, kind: 'term', text: pair.term });
cards.push({ pairId: index, kind: 'def', text: pair.match });
});
return shuffle(cards);
}
function createCard(data) {
const card = document.createElement('button');
card.type = 'button';
card.className = 'mm-card';
card.dataset.pairId = String(data.pairId);
card.innerHTML =
'<span class="mm-card-inner">' +
'<span class="mm-card-face mm-card-front">⚡</span>' +
'<span class="mm-card-face mm-card-back mm-' + data.kind + '">' +
'<span class="mm-kind">' + (data.kind === 'term' ? 'Begriff' : 'Bedeutung') + '</span>' +
'<span class="mm-text"></span>' +
'</span>' +
'</span>';
card.querySelector('.mm-text').textContent = data.text; // sicher gegen HTML im Text
card.addEventListener('click', () => onCardClick(card));
return card;
}
function renderBoard(cards) {
board.innerHTML = '';
// Spaltenzahl ~ Wurzel der Kartenzahl; die Reihenhöhe regelt das CSS (grid-auto-rows: 1fr),
// sodass das ganze Brett ohne Scrollen in die feste Stage-Höhe passt.
const cols = Math.ceil(Math.sqrt(cards.length));
board.style.gridTemplateColumns = `repeat(${cols}, minmax(0, 1fr))`;
board.style.maxWidth = (cols * 220) + 'px';
cards.forEach((data) => board.appendChild(createCard(data)));
}
function onCardClick(card) {
if (lock) return;
if (card.classList.contains('flipped') || card.classList.contains('matched')) return;
startTimer();
card.classList.add('flipped');
if (!firstCard) {
firstCard = card;
return;
}
// zweite Karte -> ein Zug
moves++;
movesEl.textContent = String(moves);
const a = firstCard;
const b = card;
if (a.dataset.pairId === b.dataset.pairId) {
a.classList.add('matched');
b.classList.add('matched');
firstCard = null;
matched++;
pairsEl.textContent = `${matched} / ${totalPairs}`;
if (matched === totalPairs) {
finish();
}
} else {
lock = true;
a.classList.add('wrong');
b.classList.add('wrong');
setTimeout(() => {
a.classList.remove('flipped', 'wrong');
b.classList.remove('flipped', 'wrong');
firstCard = null;
lock = false;
}, 900);
}
}
async function finish() {
stopTimer();
running = false;
cancelBtn.hidden = true;
const localScore = calculateScore(totalPairs, moves, seconds);
const starCount = starsFor(totalPairs, moves);
resultStars.textContent = '★'.repeat(starCount) + '☆'.repeat(3 - starCount);
resultScore.textContent = String(localScore);
resultDetail.textContent = `${totalPairs} Paare · ${moves} Züge · ${formatTime(seconds)}`;
resultHigh.hidden = true;
resultEl.hidden = false;
try {
const res = await fetch(`/api/games/${GAME}/scores`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
deck: deckSelect.value,
pairs: totalPairs,
moves: moves,
durationSeconds: seconds
})
});
if (res.ok) {
const data = await res.json();
resultScore.textContent = String(data.score); // Server ist autoritativ
resultHigh.hidden = !data.newHighscore;
bestEl.textContent = String(data.highscore);
}
} catch (err) {
console.error('Score konnte nicht gespeichert werden:', err);
}
}
/* Laufendes Spiel abbrechen: Timer stoppen, Zustand zurücksetzen, KEIN Score-POST. */
function cancelGame() {
if (!running) return;
// Rückfrage nur, wenn schon etwas passiert ist (kein Nerv-Dialog auf frischem Brett).
const touched = moves > 0 || matched > 0 || seconds > 0 || firstCard !== null;
if (touched && !window.confirm(
'Aktuelles Spiel wirklich abbrechen? Der Fortschritt geht verloren und wird nicht gespeichert.')) {
return;
}
stopTimer();
resetHud();
running = false;
firstCard = null;
lock = false;
resultEl.hidden = true;
cancelBtn.hidden = true;
setIdle('Spiel abgebrochen – wähle ein Deck und starte ein neues Spiel.');
}
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) {
/* still ohne Highscore weiter */
}
}
function newGame() {
const slug = deckSelect.value;
const deckPairs = decksBySlug.get(slug);
stopTimer();
running = false;
cancelBtn.hidden = true;
resetHud();
resultEl.hidden = true;
firstCard = null;
lock = false;
if (!deckPairs || !deckPairs.length) {
setIdle('Dieses Deck hat noch keine Paare.');
return;
}
pairs = deckPairs;
totalPairs = pairs.length;
pairsEl.textContent = `0 / ${totalPairs}`;
renderBoard(buildCards(pairs));
running = true;
cancelBtn.hidden = false;
}
async function loadDecks() {
const res = await fetch('/api/mindmatch/decks');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
async function init() {
let decks = [];
try {
decks = await loadDecks();
} catch (err) {
console.error('Decks konnten nicht geladen werden:', err);
}
deckSelect.innerHTML = '';
decksBySlug.clear();
decks.forEach((deck) => {
decksBySlug.set(deck.slug, deck.pairs || []);
const option = document.createElement('option');
option.value = deck.slug;
option.textContent = deck.title;
deckSelect.appendChild(option);
});
newBtn.addEventListener('click', newGame);
againBtn.addEventListener('click', newGame);
cancelBtn.addEventListener('click', cancelGame);
deckSelect.addEventListener('change', newGame);
await loadStats();
if (decks.length) {
newGame();
} else {
setIdle('Noch keine Decks vorhanden. Ein Admin kann sie unter „MindMatch-Decks verwalten“ anlegen.');
}
}
init();
})();
+267
View File
@@ -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();
})();
+34
View File
@@ -0,0 +1,34 @@
/* Mindflux Theme – Dark/Light-Umschaltung, zentral.
Im <head> einbinden (ohne defer), dann setzt es das Theme vor dem Rendern (kein Flash).
Jeder Button mit Klasse .mf-theme wird automatisch zum Umschalter. */
(function () {
var KEY = 'mf-theme';
function read() { try { return localStorage.getItem(KEY) || 'dark'; } catch (e) { return 'dark'; } }
function apply(mode) { document.documentElement.setAttribute('data-theme', mode); }
apply(read()); // früh -> kein Aufblitzen
var SUN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>';
var MOON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>';
function paint() {
var dark = document.documentElement.getAttribute('data-theme') === 'dark';
document.querySelectorAll('.mf-theme').forEach(function (b) { b.innerHTML = dark ? SUN : MOON; });
}
function toggle() {
var next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
apply(next);
try { localStorage.setItem(KEY, next); } catch (e) {}
paint();
}
window.MindfluxTheme = { toggle: toggle, apply: apply };
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.mf-theme').forEach(function (b) {
b.setAttribute('role', 'button');
b.setAttribute('aria-label', 'Theme umschalten');
b.addEventListener('click', toggle);
});
paint();
});
})();