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,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.');
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user