diff --git a/.gitignore b/.gitignore
index 99ef1ce..15e8892 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
node_modules/
data.json
+data/
.env
*.log
.DS_Store
diff --git a/package.json b/package.json
index fe66def..bf26c7b 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,8 @@
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
- "cors": "^2.8.5"
+ "cors": "^2.8.5",
+ "jsonwebtoken": "^9.0.2",
+ "dotenv": "^16.4.5"
}
}
diff --git a/public/app.js b/public/app.js
index af74290..327e5cb 100644
--- a/public/app.js
+++ b/public/app.js
@@ -1,15 +1,92 @@
-// API Base URL
-const API_BASE = '';
+// ─── Supabase config (injected by server from .env via /config.js) ───────────
+const supabaseClient = supabase.createClient(window.SUPABASE_URL, window.SUPABASE_ANON_KEY);
-// Globale Variablen
+// ─── App state ───────────────────────────────────────────────────────────────
let totalPoints = 0;
let tasks = [];
let chapters = [];
-let chapterDescriptions = {}; // Map von chapter name zu description
+let chapterDescriptions = {};
let activeChapter = null;
-// Initialisiere die App
-async function init() {
+// ─── Auth ────────────────────────────────────────────────────────────────────
+
+function showAuthTab(tab) {
+ document.getElementById('loginForm').style.display = tab === 'login' ? 'block' : 'none';
+ document.getElementById('registerForm').style.display = tab === 'register' ? 'block' : 'none';
+ document.getElementById('tabLogin').classList.toggle('active', tab === 'login');
+ document.getElementById('tabRegister').classList.toggle('active', tab === 'register');
+}
+
+async function login() {
+ const email = document.getElementById('loginEmail').value.trim();
+ const password = document.getElementById('loginPassword').value;
+ const errorEl = document.getElementById('loginError');
+ errorEl.textContent = '';
+
+ const { error } = await supabaseClient.auth.signInWithPassword({ email, password });
+ if (error) errorEl.textContent = error.message;
+}
+
+async function register() {
+ const name = document.getElementById('registerName').value.trim();
+ const email = document.getElementById('registerEmail').value.trim();
+ const password = document.getElementById('registerPassword').value;
+ const errorEl = document.getElementById('registerError');
+ errorEl.textContent = '';
+
+ const { data: { session }, error } = await supabaseClient.auth.signUp({
+ email,
+ password,
+ options: { data: { display_name: name || email.split('@')[0] } },
+ });
+
+ if (error) {
+ errorEl.textContent = error.message;
+ } else if (!session) {
+ // Email confirmation is enabled — user must confirm before logging in
+ errorEl.style.color = '#27ae60';
+ errorEl.textContent = 'Konto erstellt! Bitte E-Mail bestätigen, dann anmelden.';
+ }
+ // If session exists, onAuthStateChange fires and opens the app automatically
+}
+
+async function logout() {
+ await supabaseClient.auth.signOut();
+}
+
+async function getToken() {
+ const { data } = await supabaseClient.auth.getSession();
+ return data.session?.access_token;
+}
+
+// ─── Authenticated fetch wrapper ─────────────────────────────────────────────
+
+async function apiFetch(url, options = {}) {
+ const token = await getToken();
+ const res = await fetch(url, {
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`,
+ ...options.headers,
+ },
+ });
+ if (res.status === 401) {
+ await supabaseClient.auth.signOut();
+ return null;
+ }
+ return res;
+}
+
+// ─── App init ────────────────────────────────────────────────────────────────
+
+async function initApp(user) {
+ const displayName = user.user_metadata?.display_name || user.email;
+ document.getElementById('userDisplayName').textContent = displayName;
+
+ document.getElementById('authOverlay').style.display = 'none';
+ document.getElementById('appWrapper').style.display = 'flex';
+
await loadTasks();
extractChapters();
calculateTotalPoints();
@@ -18,7 +95,50 @@ async function init() {
updateProgress();
}
-// Berechne Gesamtpunktzahl aus abgeschlossenen Aufgaben
+function showAuthScreen() {
+ document.getElementById('authOverlay').style.display = 'flex';
+ document.getElementById('appWrapper').style.display = 'none';
+}
+
+// React to login/logout events
+supabaseClient.auth.onAuthStateChange((_event, session) => {
+ if (session?.user) {
+ initApp(session.user);
+ } else {
+ showAuthScreen();
+ }
+});
+
+// ─── Tasks ───────────────────────────────────────────────────────────────────
+
+async function loadTasks() {
+ const res = await apiFetch('/api/tasks');
+ if (!res) return;
+ const data = await res.json();
+ tasks = data.tasks || [];
+}
+
+function extractChapters() {
+ const chapterSet = new Set();
+ chapterDescriptions = {};
+
+ tasks.forEach(task => {
+ if (task.chapter) {
+ chapterSet.add(task.chapter);
+ if (task.chapterDescription && !chapterDescriptions[task.chapter]) {
+ chapterDescriptions[task.chapter] = task.chapterDescription;
+ }
+ }
+ });
+ chapters = Array.from(chapterSet).sort();
+
+ if (chapters.length > 0 && !activeChapter) {
+ activeChapter = chapters[0];
+ } else if (chapters.length === 0) {
+ activeChapter = null;
+ }
+}
+
function calculateTotalPoints() {
totalPoints = tasks
.filter(task => task.isCorrect === true)
@@ -26,118 +146,47 @@ function calculateTotalPoints() {
updatePointsDisplay();
}
-// Lade Aufgaben vom Server
-async function loadTasks() {
- try {
- const response = await fetch(`${API_BASE}/api/tasks`);
- const data = await response.json();
- tasks = data.tasks || [];
- } catch (error) {
- console.error('Fehler beim Laden der Aufgaben:', error);
- tasks = [];
- }
-}
-
-// Extrahiere alle Chapters aus den Tasks
-function extractChapters() {
- const chapterSet = new Set();
- chapterDescriptions = {};
-
- tasks.forEach(task => {
- if (task.chapter) {
- chapterSet.add(task.chapter);
- // Speichere die erste gefundene Beschreibung für jedes Kapitel
- if (task.chapterDescription && !chapterDescriptions[task.chapter]) {
- chapterDescriptions[task.chapter] = task.chapterDescription;
- }
- }
- });
- chapters = Array.from(chapterSet).sort();
-
- // Setze ersten Chapter als aktiv, falls vorhanden
- if (chapters.length > 0 && !activeChapter) {
- activeChapter = chapters[0];
- } else if (chapters.length === 0) {
- // Wenn keine Chapters vorhanden, zeige alle Tasks
- activeChapter = null;
- }
-}
-
-// Prüfe ob alle Aufgaben eines Kapitels gelöst sind
function isChapterCompleted(chapter) {
const chapterTasks = tasks.filter(task => task.chapter === chapter);
- if (chapterTasks.length === 0) return false;
-
- return chapterTasks.every(task => task.isCorrect === true);
+ return chapterTasks.length > 0 && chapterTasks.every(task => task.isCorrect === true);
}
-// Rendere Sidebar für alle Chapters
function renderSidebar() {
const sidebar = document.getElementById('sidebar');
sidebar.innerHTML = '';
- // Zeige Sidebar nur an, wenn es Chapters gibt
if (chapters.length === 0) {
sidebar.style.display = 'none';
return;
}
-
sidebar.style.display = 'block';
- // Sidebar Header
- const sidebarHeader = document.createElement('div');
- sidebarHeader.className = 'sidebar-header';
- const headerTitle = document.createElement('h2');
- headerTitle.textContent = 'Aktenübersicht';
- sidebarHeader.appendChild(headerTitle);
- sidebar.appendChild(sidebarHeader);
+ const header = document.createElement('div');
+ header.className = 'sidebar-header';
+ header.innerHTML = '
Aktenübersicht
';
+ sidebar.appendChild(header);
chapters.forEach(chapter => {
- const sidebarItem = document.createElement('div');
const isCompleted = isChapterCompleted(chapter);
- let itemClasses = `sidebar-item ${activeChapter === chapter ? 'active' : ''}`;
- if (isCompleted) {
- itemClasses += ' completed';
- }
- sidebarItem.className = itemClasses;
- sidebarItem.onclick = () => switchChapter(chapter);
-
- const title = document.createElement('div');
- title.className = 'sidebar-item-title';
- let titleText = chapter;
- if (isCompleted) {
- titleText = '✓ ' + titleText;
- }
- title.textContent = titleText;
- sidebarItem.appendChild(title);
-
- sidebar.appendChild(sidebarItem);
+ const item = document.createElement('div');
+ item.className = `sidebar-item ${activeChapter === chapter ? 'active' : ''} ${isCompleted ? 'completed' : ''}`;
+ item.onclick = () => switchChapter(chapter);
+ item.innerHTML = ``;
+ sidebar.appendChild(item);
});
}
-// Wechsle zu einem anderen Chapter
function switchChapter(chapter) {
activeChapter = chapter;
renderSidebar();
renderTasks();
}
-// Hole Tasks für das aktive Chapter
-function getTasksForActiveChapter() {
- if (!activeChapter) {
- return tasks;
- }
- return tasks.filter(task => task.chapter === activeChapter);
-}
-
-// Rendere Aufgaben
function renderTasks() {
const container = document.getElementById('taskContainer');
const descriptionEl = document.getElementById('chapterDescription');
-
container.innerHTML = '';
-
- // Zeige Kapitel-Beschreibung an
+
if (activeChapter && chapterDescriptions[activeChapter]) {
descriptionEl.textContent = chapterDescriptions[activeChapter];
descriptionEl.style.display = 'block';
@@ -145,64 +194,39 @@ function renderTasks() {
descriptionEl.style.display = 'none';
}
- const tasksToShow = getTasksForActiveChapter();
-
- tasksToShow.forEach(task => {
- // Überspringe bereits richtig beantwortete Aufgaben (werden nicht mehr angezeigt)
- if (task.isCorrect === true) {
- return;
- }
-
- const taskCard = document.createElement('div');
- taskCard.className = 'task-card';
- taskCard.id = `task-${task.id}`;
+ const visibleTasks = (activeChapter ? tasks.filter(t => t.chapter === activeChapter) : tasks)
+ .filter(task => task.isCorrect !== true);
+ visibleTasks.forEach(task => {
const hasAnswer = task.userAnswer !== undefined;
-
- taskCard.innerHTML = `
+ const card = document.createElement('div');
+ card.className = 'task-card';
+ card.id = `task-${task.id}`;
+ card.innerHTML = `
${task.question}
-
-
+
+
-
+
${hasAnswer && !task.isCorrect ? '✗ Berechnung fehlerhaft. Bitte erneut prüfen.' : ''}
`;
-
- if (hasAnswer && !task.isCorrect) {
- const statusEl = taskCard.querySelector('.task-status');
- statusEl.className = 'task-status error';
- }
-
- container.appendChild(taskCard);
+ container.appendChild(card);
});
}
-// Prüfe Antwort
async function checkAnswer(taskId) {
const task = tasks.find(t => t.id === taskId);
- if (!task) return;
+ if (!task || task.isCorrect === true) return;
const input = document.getElementById(`input-${taskId}`);
const status = document.getElementById(`status-${taskId}`);
- const button = input.nextElementSibling;
-
const userAnswer = parseInt(input.value);
if (isNaN(userAnswer)) {
@@ -211,106 +235,60 @@ async function checkAnswer(taskId) {
return;
}
- // Prüfe ob Aufgabe bereits korrekt beantwortet wurde
- if (task.isCorrect === true) {
- status.textContent = '✓ Fall bereits abgeschlossen.';
- status.className = 'task-status completed';
- return;
- }
+ const res = await apiFetch('/api/check-answer', {
+ method: 'POST',
+ body: JSON.stringify({ taskId, answer: userAnswer }),
+ });
+ if (!res) return;
- // Sende Antwort an Backend zur Prüfung
- try {
- const response = await fetch(`${API_BASE}/api/check-answer`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({ taskId, answer: userAnswer })
- });
+ const data = await res.json();
- const data = await response.json();
+ await loadTasks();
+ extractChapters();
+ calculateTotalPoints();
- // Lade Tasks neu, um die gespeicherte Antwort zu erhalten
- await loadTasks();
- extractChapters();
- calculateTotalPoints();
+ if (data.correct) {
+ status.textContent = '✓ Berechnung korrekt. Fall weiter bearbeiten.';
+ status.className = 'task-status success';
+ showMessage(`✓ ${data.points} Ermittlungspunkte erhalten`, 'success');
- if (data.correct) {
- status.textContent = '✓ Berechnung korrekt. Fall weiter bearbeiten.';
- status.className = 'task-status success';
-
- // Zeige Erfolgsnachricht
- showMessage(`✓ ${data.points} Ermittlungspunkte erhalten`, 'success');
-
- // Konfetti-Animation und Aufgabe verschwinden lassen
- const taskCard = document.getElementById(`task-${taskId}`);
- if (taskCard) {
- createConfetti(taskCard);
- taskCard.classList.add('confetti-animation');
-
- // Nach Animation die Aufgabe entfernen
- setTimeout(() => {
- renderSidebar();
- renderTasks();
- updateProgress();
- }, 1500);
- }
- } else {
- status.textContent = '✗ Berechnung fehlerhaft. Bitte erneut prüfen.';
- status.className = 'task-status error';
- input.focus();
+ const taskCard = document.getElementById(`task-${taskId}`);
+ if (taskCard) {
+ createConfetti(taskCard);
+ taskCard.classList.add('confetti-animation');
+ setTimeout(() => { renderSidebar(); renderTasks(); updateProgress(); }, 1500);
}
-
- // Rendere Sidebar und Tasks neu, um den aktuellen Status anzuzeigen (nur wenn nicht korrekt)
- if (!data.correct) {
- renderSidebar();
- renderTasks();
- updateProgress();
- }
- } catch (error) {
- console.error('Fehler beim Prüfen der Antwort:', error);
- status.textContent = 'Systemfehler. Bitte erneut versuchen.';
+ } else {
+ status.textContent = '✗ Berechnung fehlerhaft. Bitte erneut prüfen.';
status.className = 'task-status error';
+ input.focus();
+ renderSidebar();
+ renderTasks();
+ updateProgress();
}
}
-// Aktualisiere Punkte-Anzeige
+// ─── UI helpers ───────────────────────────────────────────────────────────────
+
function updatePointsDisplay() {
document.getElementById('totalPoints').textContent = totalPoints;
}
-// Berechne maximale mögliche Punkte (Summe aller Tasks)
-function calculateMaxPoints() {
- return tasks.reduce((sum, task) => sum + (task.points || 0), 0);
-}
-
-// Aktualisiere Progress-Balken
function updateProgress() {
- const maxPoints = calculateMaxPoints();
+ const maxPoints = tasks.reduce((sum, task) => sum + (task.points || 0), 0);
const percentage = maxPoints > 0 ? Math.min((totalPoints / maxPoints) * 100, 100) : 0;
-
- const progressBar = document.getElementById('progressBar');
- const progressText = document.getElementById('progressText');
-
- progressBar.style.width = `${percentage}%`;
- progressText.textContent = `${totalPoints} / ${maxPoints}`;
+ document.getElementById('progressBar').style.width = `${percentage}%`;
+ document.getElementById('progressText').textContent = `${totalPoints} / ${maxPoints}`;
}
-// Erstelle Konfetti-Animation
function createConfetti(element) {
const colors = ['#3498db', '#2980b9', '#34495e', '#2c3e50', '#1a252f', '#27ae60', '#16a085', '#7f8c8d'];
- const confettiCount = 50;
-
- // Hole Position des Elements
const rect = element.getBoundingClientRect();
- const container = element.closest('.container') || document.body;
-
- for (let i = 0; i < confettiCount; i++) {
+
+ for (let i = 0; i < 50; i++) {
const confetti = document.createElement('div');
confetti.className = 'confetti';
-
- // Zufällige Position relativ zum Element
- const randomX = (Math.random() - 0.5) * 2; // -1 bis 1
+ const randomX = (Math.random() - 0.5) * 2;
confetti.style.left = (rect.left + rect.width / 2 + (Math.random() - 0.5) * rect.width) + 'px';
confetti.style.top = (rect.top - 10) + 'px';
confetti.style.background = colors[Math.floor(Math.random() * colors.length)];
@@ -321,41 +299,27 @@ function createConfetti(element) {
confetti.style.borderRadius = Math.random() > 0.5 ? '50%' : '0%';
confetti.style.setProperty('--random-x', randomX);
confetti.style.position = 'fixed';
-
document.body.appendChild(confetti);
-
- // Entferne Konfetti nach Animation
- setTimeout(() => {
- if (confetti.parentNode) {
- confetti.remove();
- }
- }, 2000);
+ setTimeout(() => confetti.remove(), 2000);
}
}
-// Zeige Nachricht
function showMessage(text, type) {
const message = document.getElementById('message');
message.textContent = text;
message.className = `message ${type} show`;
-
- setTimeout(() => {
- message.classList.remove('show');
- }, 3000);
+ setTimeout(() => message.classList.remove('show'), 3000);
}
-// Enter-Taste für Input-Felder
-document.addEventListener('DOMContentLoaded', () => {
- init();
-
- // Event Listener für Enter-Taste
- document.addEventListener('keypress', (e) => {
- if (e.key === 'Enter') {
- const input = document.activeElement;
- if (input && input.classList.contains('task-input')) {
- const taskId = input.id.replace('input-', '');
- checkAnswer(taskId);
- }
+// ─── Enter key support ───────────────────────────────────────────────────────
+
+document.addEventListener('keypress', e => {
+ if (e.key === 'Enter') {
+ const input = document.activeElement;
+ if (input?.classList.contains('task-input')) {
+ checkAnswer(input.id.replace('input-', ''));
}
- });
+ if (input?.id === 'loginPassword') login();
+ if (input?.id === 'registerPassword') register();
+ }
});
diff --git a/public/index.html b/public/index.html
index fa2b8a1..f96d88b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -7,18 +7,52 @@
-
-
+
+
+
+
+
Kriminalfälle
+
Mathematische Ermittlungen
+
+
+
+
+
+
+
+
+
+
+
+
+
+