implement supabase
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
data.json
|
data.json
|
||||||
|
data/
|
||||||
.env
|
.env
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"cors": "^2.8.5"
|
"cors": "^2.8.5",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"dotenv": "^16.4.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
386
public/app.js
386
public/app.js
@@ -1,15 +1,92 @@
|
|||||||
// API Base URL
|
// ─── Supabase config (injected by server from .env via /config.js) ───────────
|
||||||
const API_BASE = '';
|
const supabaseClient = supabase.createClient(window.SUPABASE_URL, window.SUPABASE_ANON_KEY);
|
||||||
|
|
||||||
// Globale Variablen
|
// ─── App state ───────────────────────────────────────────────────────────────
|
||||||
let totalPoints = 0;
|
let totalPoints = 0;
|
||||||
let tasks = [];
|
let tasks = [];
|
||||||
let chapters = [];
|
let chapters = [];
|
||||||
let chapterDescriptions = {}; // Map von chapter name zu description
|
let chapterDescriptions = {};
|
||||||
let activeChapter = null;
|
let activeChapter = null;
|
||||||
|
|
||||||
// Initialisiere die App
|
// ─── Auth ────────────────────────────────────────────────────────────────────
|
||||||
async function init() {
|
|
||||||
|
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();
|
await loadTasks();
|
||||||
extractChapters();
|
extractChapters();
|
||||||
calculateTotalPoints();
|
calculateTotalPoints();
|
||||||
@@ -18,27 +95,29 @@ async function init() {
|
|||||||
updateProgress();
|
updateProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Berechne Gesamtpunktzahl aus abgeschlossenen Aufgaben
|
function showAuthScreen() {
|
||||||
function calculateTotalPoints() {
|
document.getElementById('authOverlay').style.display = 'flex';
|
||||||
totalPoints = tasks
|
document.getElementById('appWrapper').style.display = 'none';
|
||||||
.filter(task => task.isCorrect === true)
|
|
||||||
.reduce((sum, task) => sum + (task.points || 0), 0);
|
|
||||||
updatePointsDisplay();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lade Aufgaben vom Server
|
// React to login/logout events
|
||||||
async function loadTasks() {
|
supabaseClient.auth.onAuthStateChange((_event, session) => {
|
||||||
try {
|
if (session?.user) {
|
||||||
const response = await fetch(`${API_BASE}/api/tasks`);
|
initApp(session.user);
|
||||||
const data = await response.json();
|
} else {
|
||||||
tasks = data.tasks || [];
|
showAuthScreen();
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Laden der Aufgaben:', error);
|
|
||||||
tasks = [];
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Tasks ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function loadTasks() {
|
||||||
|
const res = await apiFetch('/api/tasks');
|
||||||
|
if (!res) return;
|
||||||
|
const data = await res.json();
|
||||||
|
tasks = data.tasks || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extrahiere alle Chapters aus den Tasks
|
|
||||||
function extractChapters() {
|
function extractChapters() {
|
||||||
const chapterSet = new Set();
|
const chapterSet = new Set();
|
||||||
chapterDescriptions = {};
|
chapterDescriptions = {};
|
||||||
@@ -46,7 +125,6 @@ function extractChapters() {
|
|||||||
tasks.forEach(task => {
|
tasks.forEach(task => {
|
||||||
if (task.chapter) {
|
if (task.chapter) {
|
||||||
chapterSet.add(task.chapter);
|
chapterSet.add(task.chapter);
|
||||||
// Speichere die erste gefundene Beschreibung für jedes Kapitel
|
|
||||||
if (task.chapterDescription && !chapterDescriptions[task.chapter]) {
|
if (task.chapterDescription && !chapterDescriptions[task.chapter]) {
|
||||||
chapterDescriptions[task.chapter] = task.chapterDescription;
|
chapterDescriptions[task.chapter] = task.chapterDescription;
|
||||||
}
|
}
|
||||||
@@ -54,90 +132,61 @@ function extractChapters() {
|
|||||||
});
|
});
|
||||||
chapters = Array.from(chapterSet).sort();
|
chapters = Array.from(chapterSet).sort();
|
||||||
|
|
||||||
// Setze ersten Chapter als aktiv, falls vorhanden
|
|
||||||
if (chapters.length > 0 && !activeChapter) {
|
if (chapters.length > 0 && !activeChapter) {
|
||||||
activeChapter = chapters[0];
|
activeChapter = chapters[0];
|
||||||
} else if (chapters.length === 0) {
|
} else if (chapters.length === 0) {
|
||||||
// Wenn keine Chapters vorhanden, zeige alle Tasks
|
|
||||||
activeChapter = null;
|
activeChapter = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfe ob alle Aufgaben eines Kapitels gelöst sind
|
function calculateTotalPoints() {
|
||||||
function isChapterCompleted(chapter) {
|
totalPoints = tasks
|
||||||
const chapterTasks = tasks.filter(task => task.chapter === chapter);
|
.filter(task => task.isCorrect === true)
|
||||||
if (chapterTasks.length === 0) return false;
|
.reduce((sum, task) => sum + (task.points || 0), 0);
|
||||||
|
updatePointsDisplay();
|
||||||
return chapterTasks.every(task => task.isCorrect === true);
|
}
|
||||||
|
|
||||||
|
function isChapterCompleted(chapter) {
|
||||||
|
const chapterTasks = tasks.filter(task => task.chapter === chapter);
|
||||||
|
return chapterTasks.length > 0 && chapterTasks.every(task => task.isCorrect === true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rendere Sidebar für alle Chapters
|
|
||||||
function renderSidebar() {
|
function renderSidebar() {
|
||||||
const sidebar = document.getElementById('sidebar');
|
const sidebar = document.getElementById('sidebar');
|
||||||
sidebar.innerHTML = '';
|
sidebar.innerHTML = '';
|
||||||
|
|
||||||
// Zeige Sidebar nur an, wenn es Chapters gibt
|
|
||||||
if (chapters.length === 0) {
|
if (chapters.length === 0) {
|
||||||
sidebar.style.display = 'none';
|
sidebar.style.display = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sidebar.style.display = 'block';
|
sidebar.style.display = 'block';
|
||||||
|
|
||||||
// Sidebar Header
|
const header = document.createElement('div');
|
||||||
const sidebarHeader = document.createElement('div');
|
header.className = 'sidebar-header';
|
||||||
sidebarHeader.className = 'sidebar-header';
|
header.innerHTML = '<h2>Aktenübersicht</h2>';
|
||||||
const headerTitle = document.createElement('h2');
|
sidebar.appendChild(header);
|
||||||
headerTitle.textContent = 'Aktenübersicht';
|
|
||||||
sidebarHeader.appendChild(headerTitle);
|
|
||||||
sidebar.appendChild(sidebarHeader);
|
|
||||||
|
|
||||||
chapters.forEach(chapter => {
|
chapters.forEach(chapter => {
|
||||||
const sidebarItem = document.createElement('div');
|
|
||||||
const isCompleted = isChapterCompleted(chapter);
|
const isCompleted = isChapterCompleted(chapter);
|
||||||
let itemClasses = `sidebar-item ${activeChapter === chapter ? 'active' : ''}`;
|
const item = document.createElement('div');
|
||||||
if (isCompleted) {
|
item.className = `sidebar-item ${activeChapter === chapter ? 'active' : ''} ${isCompleted ? 'completed' : ''}`;
|
||||||
itemClasses += ' completed';
|
item.onclick = () => switchChapter(chapter);
|
||||||
}
|
item.innerHTML = `<div class="sidebar-item-title">${isCompleted ? '✓ ' : ''}${chapter}</div>`;
|
||||||
sidebarItem.className = itemClasses;
|
sidebar.appendChild(item);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wechsle zu einem anderen Chapter
|
|
||||||
function switchChapter(chapter) {
|
function switchChapter(chapter) {
|
||||||
activeChapter = chapter;
|
activeChapter = chapter;
|
||||||
renderSidebar();
|
renderSidebar();
|
||||||
renderTasks();
|
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() {
|
function renderTasks() {
|
||||||
const container = document.getElementById('taskContainer');
|
const container = document.getElementById('taskContainer');
|
||||||
const descriptionEl = document.getElementById('chapterDescription');
|
const descriptionEl = document.getElementById('chapterDescription');
|
||||||
|
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
// Zeige Kapitel-Beschreibung an
|
|
||||||
if (activeChapter && chapterDescriptions[activeChapter]) {
|
if (activeChapter && chapterDescriptions[activeChapter]) {
|
||||||
descriptionEl.textContent = chapterDescriptions[activeChapter];
|
descriptionEl.textContent = chapterDescriptions[activeChapter];
|
||||||
descriptionEl.style.display = 'block';
|
descriptionEl.style.display = 'block';
|
||||||
@@ -145,64 +194,39 @@ function renderTasks() {
|
|||||||
descriptionEl.style.display = 'none';
|
descriptionEl.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
const tasksToShow = getTasksForActiveChapter();
|
const visibleTasks = (activeChapter ? tasks.filter(t => t.chapter === activeChapter) : tasks)
|
||||||
|
.filter(task => task.isCorrect !== true);
|
||||||
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}`;
|
|
||||||
|
|
||||||
|
visibleTasks.forEach(task => {
|
||||||
const hasAnswer = task.userAnswer !== undefined;
|
const hasAnswer = task.userAnswer !== undefined;
|
||||||
|
const card = document.createElement('div');
|
||||||
taskCard.innerHTML = `
|
card.className = 'task-card';
|
||||||
|
card.id = `task-${task.id}`;
|
||||||
|
card.innerHTML = `
|
||||||
<div class="task-header">
|
<div class="task-header">
|
||||||
<div class="task-title">Ermittlungsaufgabe</div>
|
<div class="task-title">Ermittlungsaufgabe</div>
|
||||||
<div class="task-points">${task.points} EP</div>
|
<div class="task-points">${task.points} EP</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="task-question">${task.question}</div>
|
<div class="task-question">${task.question}</div>
|
||||||
<div class="task-input-group">
|
<div class="task-input-group">
|
||||||
<input
|
<input type="number" class="task-input" id="input-${task.id}"
|
||||||
type="number"
|
placeholder="?" value="${hasAnswer ? task.userAnswer : ''}">
|
||||||
class="task-input"
|
<button class="task-button" onclick="checkAnswer('${task.id}')">Prüfen</button>
|
||||||
id="input-${task.id}"
|
|
||||||
placeholder="?"
|
|
||||||
value="${hasAnswer ? task.userAnswer : ''}"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="task-button"
|
|
||||||
onclick="checkAnswer('${task.id}')"
|
|
||||||
>
|
|
||||||
Prüfen
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="task-status" id="status-${task.id}">
|
<div class="task-status ${hasAnswer && !task.isCorrect ? 'error' : ''}" id="status-${task.id}">
|
||||||
${hasAnswer && !task.isCorrect ? '✗ Berechnung fehlerhaft. Bitte erneut prüfen.' : ''}
|
${hasAnswer && !task.isCorrect ? '✗ Berechnung fehlerhaft. Bitte erneut prüfen.' : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
container.appendChild(card);
|
||||||
if (hasAnswer && !task.isCorrect) {
|
|
||||||
const statusEl = taskCard.querySelector('.task-status');
|
|
||||||
statusEl.className = 'task-status error';
|
|
||||||
}
|
|
||||||
|
|
||||||
container.appendChild(taskCard);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfe Antwort
|
|
||||||
async function checkAnswer(taskId) {
|
async function checkAnswer(taskId) {
|
||||||
const task = tasks.find(t => t.id === 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 input = document.getElementById(`input-${taskId}`);
|
||||||
const status = document.getElementById(`status-${taskId}`);
|
const status = document.getElementById(`status-${taskId}`);
|
||||||
const button = input.nextElementSibling;
|
|
||||||
|
|
||||||
const userAnswer = parseInt(input.value);
|
const userAnswer = parseInt(input.value);
|
||||||
|
|
||||||
if (isNaN(userAnswer)) {
|
if (isNaN(userAnswer)) {
|
||||||
@@ -211,106 +235,60 @@ async function checkAnswer(taskId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfe ob Aufgabe bereits korrekt beantwortet wurde
|
const res = await apiFetch('/api/check-answer', {
|
||||||
if (task.isCorrect === true) {
|
method: 'POST',
|
||||||
status.textContent = '✓ Fall bereits abgeschlossen.';
|
body: JSON.stringify({ taskId, answer: userAnswer }),
|
||||||
status.className = 'task-status completed';
|
});
|
||||||
return;
|
if (!res) return;
|
||||||
}
|
|
||||||
|
|
||||||
// Sende Antwort an Backend zur Prüfung
|
const data = await res.json();
|
||||||
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 response.json();
|
await loadTasks();
|
||||||
|
extractChapters();
|
||||||
|
calculateTotalPoints();
|
||||||
|
|
||||||
// Lade Tasks neu, um die gespeicherte Antwort zu erhalten
|
if (data.correct) {
|
||||||
await loadTasks();
|
status.textContent = '✓ Berechnung korrekt. Fall weiter bearbeiten.';
|
||||||
extractChapters();
|
status.className = 'task-status success';
|
||||||
calculateTotalPoints();
|
showMessage(`✓ ${data.points} Ermittlungspunkte erhalten`, 'success');
|
||||||
|
|
||||||
if (data.correct) {
|
const taskCard = document.getElementById(`task-${taskId}`);
|
||||||
status.textContent = '✓ Berechnung korrekt. Fall weiter bearbeiten.';
|
if (taskCard) {
|
||||||
status.className = 'task-status success';
|
createConfetti(taskCard);
|
||||||
|
taskCard.classList.add('confetti-animation');
|
||||||
// Zeige Erfolgsnachricht
|
setTimeout(() => { renderSidebar(); renderTasks(); updateProgress(); }, 1500);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// Rendere Sidebar und Tasks neu, um den aktuellen Status anzuzeigen (nur wenn nicht korrekt)
|
status.textContent = '✗ Berechnung fehlerhaft. Bitte erneut prüfen.';
|
||||||
if (!data.correct) {
|
|
||||||
renderSidebar();
|
|
||||||
renderTasks();
|
|
||||||
updateProgress();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Prüfen der Antwort:', error);
|
|
||||||
status.textContent = 'Systemfehler. Bitte erneut versuchen.';
|
|
||||||
status.className = 'task-status error';
|
status.className = 'task-status error';
|
||||||
|
input.focus();
|
||||||
|
renderSidebar();
|
||||||
|
renderTasks();
|
||||||
|
updateProgress();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aktualisiere Punkte-Anzeige
|
// ─── UI helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function updatePointsDisplay() {
|
function updatePointsDisplay() {
|
||||||
document.getElementById('totalPoints').textContent = totalPoints;
|
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() {
|
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 percentage = maxPoints > 0 ? Math.min((totalPoints / maxPoints) * 100, 100) : 0;
|
||||||
|
document.getElementById('progressBar').style.width = `${percentage}%`;
|
||||||
const progressBar = document.getElementById('progressBar');
|
document.getElementById('progressText').textContent = `${totalPoints} / ${maxPoints}`;
|
||||||
const progressText = document.getElementById('progressText');
|
|
||||||
|
|
||||||
progressBar.style.width = `${percentage}%`;
|
|
||||||
progressText.textContent = `${totalPoints} / ${maxPoints}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Erstelle Konfetti-Animation
|
|
||||||
function createConfetti(element) {
|
function createConfetti(element) {
|
||||||
const colors = ['#3498db', '#2980b9', '#34495e', '#2c3e50', '#1a252f', '#27ae60', '#16a085', '#7f8c8d'];
|
const colors = ['#3498db', '#2980b9', '#34495e', '#2c3e50', '#1a252f', '#27ae60', '#16a085', '#7f8c8d'];
|
||||||
const confettiCount = 50;
|
|
||||||
|
|
||||||
// Hole Position des Elements
|
|
||||||
const rect = element.getBoundingClientRect();
|
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');
|
const confetti = document.createElement('div');
|
||||||
confetti.className = 'confetti';
|
confetti.className = 'confetti';
|
||||||
|
const randomX = (Math.random() - 0.5) * 2;
|
||||||
// Zufällige Position relativ zum Element
|
|
||||||
const randomX = (Math.random() - 0.5) * 2; // -1 bis 1
|
|
||||||
confetti.style.left = (rect.left + rect.width / 2 + (Math.random() - 0.5) * rect.width) + 'px';
|
confetti.style.left = (rect.left + rect.width / 2 + (Math.random() - 0.5) * rect.width) + 'px';
|
||||||
confetti.style.top = (rect.top - 10) + 'px';
|
confetti.style.top = (rect.top - 10) + 'px';
|
||||||
confetti.style.background = colors[Math.floor(Math.random() * colors.length)];
|
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.borderRadius = Math.random() > 0.5 ? '50%' : '0%';
|
||||||
confetti.style.setProperty('--random-x', randomX);
|
confetti.style.setProperty('--random-x', randomX);
|
||||||
confetti.style.position = 'fixed';
|
confetti.style.position = 'fixed';
|
||||||
|
|
||||||
document.body.appendChild(confetti);
|
document.body.appendChild(confetti);
|
||||||
|
setTimeout(() => confetti.remove(), 2000);
|
||||||
// Entferne Konfetti nach Animation
|
|
||||||
setTimeout(() => {
|
|
||||||
if (confetti.parentNode) {
|
|
||||||
confetti.remove();
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zeige Nachricht
|
|
||||||
function showMessage(text, type) {
|
function showMessage(text, type) {
|
||||||
const message = document.getElementById('message');
|
const message = document.getElementById('message');
|
||||||
message.textContent = text;
|
message.textContent = text;
|
||||||
message.className = `message ${type} show`;
|
message.className = `message ${type} show`;
|
||||||
|
setTimeout(() => message.classList.remove('show'), 3000);
|
||||||
setTimeout(() => {
|
|
||||||
message.classList.remove('show');
|
|
||||||
}, 3000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enter-Taste für Input-Felder
|
// ─── Enter key support ───────────────────────────────────────────────────────
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
init();
|
|
||||||
|
|
||||||
// Event Listener für Enter-Taste
|
document.addEventListener('keypress', e => {
|
||||||
document.addEventListener('keypress', (e) => {
|
if (e.key === 'Enter') {
|
||||||
if (e.key === 'Enter') {
|
const input = document.activeElement;
|
||||||
const input = document.activeElement;
|
if (input?.classList.contains('task-input')) {
|
||||||
if (input && input.classList.contains('task-input')) {
|
checkAnswer(input.id.replace('input-', ''));
|
||||||
const taskId = input.id.replace('input-', '');
|
|
||||||
checkAnswer(taskId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
if (input?.id === 'loginPassword') login();
|
||||||
|
if (input?.id === 'registerPassword') register();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,18 +7,52 @@
|
|||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-wrapper">
|
|
||||||
<aside class="sidebar" id="sidebar">
|
<!-- Auth overlay — shown when not logged in -->
|
||||||
<!-- Sidebar wird hier dynamisch eingefügt -->
|
<div id="authOverlay" class="auth-overlay">
|
||||||
</aside>
|
<div class="auth-card">
|
||||||
|
<h1 class="auth-title">Kriminalfälle</h1>
|
||||||
|
<p class="auth-subtitle">Mathematische Ermittlungen</p>
|
||||||
|
|
||||||
|
<div class="auth-tabs">
|
||||||
|
<button class="auth-tab active" id="tabLogin" onclick="showAuthTab('login')">Anmelden</button>
|
||||||
|
<button class="auth-tab" id="tabRegister" onclick="showAuthTab('register')">Registrieren</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="loginForm">
|
||||||
|
<input type="email" id="loginEmail" class="auth-input" placeholder="E-Mail-Adresse" autocomplete="email">
|
||||||
|
<input type="password" id="loginPassword" class="auth-input" placeholder="Passwort" autocomplete="current-password">
|
||||||
|
<div class="auth-error" id="loginError"></div>
|
||||||
|
<button class="auth-button" onclick="login()">Anmelden</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="registerForm" style="display:none">
|
||||||
|
<input type="text" id="registerName" class="auth-input" placeholder="Anzeigename (optional)" autocomplete="name">
|
||||||
|
<input type="email" id="registerEmail" class="auth-input" placeholder="E-Mail-Adresse" autocomplete="email">
|
||||||
|
<input type="password" id="registerPassword" class="auth-input" placeholder="Passwort (min. 6 Zeichen)" autocomplete="new-password">
|
||||||
|
<div class="auth-error" id="registerError"></div>
|
||||||
|
<button class="auth-button" onclick="register()">Registrieren</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main app — hidden until logged in -->
|
||||||
|
<div id="appWrapper" class="app-wrapper" style="display:none">
|
||||||
|
<aside class="sidebar" id="sidebar"></aside>
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header>
|
<header>
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<h1>Kriminalfälle - Mathematische Ermittlungen</h1>
|
<h1>Kriminalfälle - Mathematische Ermittlungen</h1>
|
||||||
<div class="points-display">
|
<div class="header-right">
|
||||||
<div class="points-label">Ermittlungspunkte</div>
|
<div class="points-display">
|
||||||
<div class="points-value" id="totalPoints">0</div>
|
<div class="points-label">Ermittlungspunkte</div>
|
||||||
|
<div class="points-value" id="totalPoints">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-display">
|
||||||
|
<div class="user-name" id="userDisplayName"></div>
|
||||||
|
<button class="logout-button" onclick="logout()">Abmelden</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -35,9 +69,7 @@
|
|||||||
|
|
||||||
<div class="task-section">
|
<div class="task-section">
|
||||||
<div id="chapterDescription" class="chapter-description"></div>
|
<div id="chapterDescription" class="chapter-description"></div>
|
||||||
<div id="taskContainer" class="task-container">
|
<div id="taskContainer" class="task-container"></div>
|
||||||
<!-- Aufgaben werden hier dynamisch eingefügt -->
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="message" id="message"></div>
|
<div class="message" id="message"></div>
|
||||||
@@ -45,6 +77,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="/config.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.js"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
151
public/style.css
151
public/style.css
@@ -405,6 +405,157 @@ h1 {
|
|||||||
border-color: #c0392b;
|
border-color: #c0392b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Auth overlay ─────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.auth-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: #1a252f;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
background: #2c3e50;
|
||||||
|
border: 2px solid #34495e;
|
||||||
|
padding: 40px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-title {
|
||||||
|
color: #ecf0f1;
|
||||||
|
font-size: 1.6em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-subtitle {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-tabs {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border-bottom: 2px solid #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-tab {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #7f8c8d;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 0.95em;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-tab.active {
|
||||||
|
color: #3498db;
|
||||||
|
border-bottom-color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background: #1a252f;
|
||||||
|
border: 2px solid #34495e;
|
||||||
|
color: #ecf0f1;
|
||||||
|
font-size: 1em;
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-input::placeholder {
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-error {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #e74c3c;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-button:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── User display in header ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-display {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
color: #bdc3c7;
|
||||||
|
font-size: 0.85em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-button {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #34495e;
|
||||||
|
color: #7f8c8d;
|
||||||
|
padding: 5px 12px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-button:hover {
|
||||||
|
border-color: #e74c3c;
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.app-wrapper {
|
.app-wrapper {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
293
server.js
293
server.js
@@ -1,196 +1,161 @@
|
|||||||
const express = require('express');
|
require("dotenv").config();
|
||||||
const cors = require('cors');
|
const express = require("express");
|
||||||
const fs = require('fs');
|
const cors = require("cors");
|
||||||
const path = require('path');
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
const CASES_DIR = path.join(__dirname, 'cases');
|
const CASES_DIR = path.join(__dirname, "cases");
|
||||||
|
const PROGRESS_DIR = path.join(__dirname, "data", "progress");
|
||||||
|
|
||||||
|
// Ensure progress directory exists
|
||||||
|
if (!fs.existsSync(PROGRESS_DIR)) {
|
||||||
|
fs.mkdirSync(PROGRESS_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.static('public'));
|
|
||||||
|
|
||||||
// Lade alle Aufgaben aus dem cases-Ordner
|
// Expose public Supabase config to the frontend
|
||||||
function loadTasks() {
|
app.get("/config.js", (_req, res) => {
|
||||||
try {
|
res.type("application/javascript");
|
||||||
// Erstelle cases-Ordner falls er nicht existiert
|
res.send(
|
||||||
if (!fs.existsSync(CASES_DIR)) {
|
`window.SUPABASE_URL = '${process.env.SUPABASE_URL}'; window.SUPABASE_ANON_KEY = '${process.env.SUPABASE_ANON_KEY}';`,
|
||||||
fs.mkdirSync(CASES_DIR, { recursive: true });
|
);
|
||||||
console.log('Cases-Ordner erstellt:', CASES_DIR);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const allTasks = [];
|
|
||||||
const files = fs.readdirSync(CASES_DIR);
|
|
||||||
|
|
||||||
files.forEach(file => {
|
|
||||||
if (file.endsWith('.json')) {
|
|
||||||
const filePath = path.join(CASES_DIR, file);
|
|
||||||
try {
|
|
||||||
const data = fs.readFileSync(filePath, 'utf8');
|
|
||||||
if (!data || !data.trim()) {
|
|
||||||
console.warn('Leere Datei übersprungen:', file);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const caseData = JSON.parse(data);
|
|
||||||
// Unterstütze sowohl alte Struktur (Array) als auch neue Struktur (Objekt)
|
|
||||||
if (Array.isArray(caseData)) {
|
|
||||||
allTasks.push(...caseData);
|
|
||||||
} else if (caseData.tasks && Array.isArray(caseData.tasks)) {
|
|
||||||
caseData.tasks.forEach(task => {
|
|
||||||
const taskWithChapter = {
|
|
||||||
...task,
|
|
||||||
chapter: caseData.chapter,
|
|
||||||
chapterDescription: caseData.description
|
|
||||||
};
|
|
||||||
allTasks.push(taskWithChapter);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Fehler beim Laden von', file, ':', err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return allTasks;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Laden der Aufgaben:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finde die Datei, die eine bestimmte Task-ID enthält
|
|
||||||
function findTaskFile(taskId) {
|
|
||||||
try {
|
|
||||||
if (!fs.existsSync(CASES_DIR)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = fs.readdirSync(CASES_DIR);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
if (file.endsWith('.json')) {
|
|
||||||
const filePath = path.join(CASES_DIR, file);
|
|
||||||
const data = fs.readFileSync(filePath, 'utf8');
|
|
||||||
const caseData = JSON.parse(data);
|
|
||||||
|
|
||||||
// Unterstütze sowohl alte Struktur (Array) als auch neue Struktur (Objekt)
|
|
||||||
let tasks = [];
|
|
||||||
if (Array.isArray(caseData)) {
|
|
||||||
tasks = caseData;
|
|
||||||
} else if (caseData.tasks && Array.isArray(caseData.tasks)) {
|
|
||||||
tasks = caseData.tasks;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tasks.some(task => task.id === taskId)) {
|
|
||||||
return { filePath, caseData };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Finden der Task-Datei:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Speichere Aufgaben in der entsprechenden Datei
|
|
||||||
function saveTask(taskId, updatedTask) {
|
|
||||||
try {
|
|
||||||
const fileInfo = findTaskFile(taskId);
|
|
||||||
if (!fileInfo) {
|
|
||||||
throw new Error(`Datei für Task ${taskId} nicht gefunden`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { filePath, caseData } = fileInfo;
|
|
||||||
|
|
||||||
// Entferne chapter und chapterDescription aus der Task (werden beim Laden wieder hinzugefügt)
|
|
||||||
const { chapter, chapterDescription, ...taskWithoutChapter } = updatedTask;
|
|
||||||
|
|
||||||
// Unterstütze sowohl alte Struktur (Array) als auch neue Struktur (Objekt)
|
|
||||||
if (Array.isArray(caseData)) {
|
|
||||||
// Alte Struktur: direktes Array
|
|
||||||
const taskIndex = caseData.findIndex(t => t.id === taskId);
|
|
||||||
if (taskIndex === -1) {
|
|
||||||
throw new Error(`Task ${taskId} nicht in Datei gefunden`);
|
|
||||||
}
|
|
||||||
caseData[taskIndex] = updatedTask;
|
|
||||||
fs.writeFileSync(filePath, JSON.stringify(caseData, null, 2));
|
|
||||||
} else if (caseData.tasks && Array.isArray(caseData.tasks)) {
|
|
||||||
// Neue Struktur: Objekt mit chapter, description, tasks
|
|
||||||
const taskIndex = caseData.tasks.findIndex(t => t.id === taskId);
|
|
||||||
if (taskIndex === -1) {
|
|
||||||
throw new Error(`Task ${taskId} nicht in Datei gefunden`);
|
|
||||||
}
|
|
||||||
caseData.tasks[taskIndex] = taskWithoutChapter;
|
|
||||||
fs.writeFileSync(filePath, JSON.stringify(caseData, null, 2));
|
|
||||||
} else {
|
|
||||||
throw new Error('Ungültige Dateistruktur');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Speichern der Aufgabe:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// API: Hole alle Aufgaben
|
|
||||||
app.get('/api/tasks', (req, res) => {
|
|
||||||
const tasks = loadTasks();
|
|
||||||
res.json({ tasks });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.use(express.static("public"));
|
||||||
|
|
||||||
// API: Prüfe Antwort
|
// Verify token by asking Supabase directly — works regardless of JWT algorithm
|
||||||
app.post('/api/check-answer', (req, res) => {
|
async function requireAuth(req, res, next) {
|
||||||
|
const auth = req.headers.authorization;
|
||||||
|
if (!auth || !auth.startsWith("Bearer ")) {
|
||||||
|
return res.status(401).json({ error: "Nicht angemeldet" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${process.env.SUPABASE_URL}/auth/v1/user`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: auth,
|
||||||
|
apikey: process.env.SUPABASE_ANON_KEY,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error("[auth] Supabase rejected token, status:", response.status);
|
||||||
|
return res
|
||||||
|
.status(401)
|
||||||
|
.json({ error: "Sitzung abgelaufen. Bitte erneut anmelden." });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await response.json();
|
||||||
|
console.log("[auth] OK, userId:", user.id);
|
||||||
|
req.userId = user.id;
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[auth] Supabase request failed:", err.message);
|
||||||
|
res.status(401).json({ error: "Authentifizierung fehlgeschlagen." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load task definitions from case files (no user progress mixed in)
|
||||||
|
function loadCaseTasks() {
|
||||||
|
if (!fs.existsSync(CASES_DIR)) return [];
|
||||||
|
const allTasks = [];
|
||||||
|
|
||||||
|
fs.readdirSync(CASES_DIR).forEach((file) => {
|
||||||
|
if (!file.endsWith(".json")) return;
|
||||||
|
try {
|
||||||
|
const caseData = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(CASES_DIR, file), "utf8"),
|
||||||
|
);
|
||||||
|
if (Array.isArray(caseData)) {
|
||||||
|
caseData.forEach(
|
||||||
|
({ userAnswer, isCorrect, attempts, answerTimestamp, ...task }) => {
|
||||||
|
allTasks.push(task);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (caseData.tasks) {
|
||||||
|
caseData.tasks.forEach(
|
||||||
|
({ userAnswer, isCorrect, attempts, answerTimestamp, ...task }) => {
|
||||||
|
allTasks.push({
|
||||||
|
...task,
|
||||||
|
chapter: caseData.chapter,
|
||||||
|
chapterDescription: caseData.description,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Fehler beim Laden von", file, ":", err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return allTasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load/save per-user progress from data/progress/{userId}.json
|
||||||
|
function loadProgress(userId) {
|
||||||
|
const file = path.join(PROGRESS_DIR, `${userId}.json`);
|
||||||
|
if (!fs.existsSync(file)) return {};
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveProgress(userId, progress) {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(PROGRESS_DIR, `${userId}.json`),
|
||||||
|
JSON.stringify(progress, null, 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// API: Get all tasks merged with user's progress
|
||||||
|
app.get("/api/tasks", requireAuth, (req, res) => {
|
||||||
|
const tasks = loadCaseTasks();
|
||||||
|
const progress = loadProgress(req.userId);
|
||||||
|
res.json({ tasks: tasks.map((task) => ({ ...task, ...progress[task.id] })) });
|
||||||
|
});
|
||||||
|
|
||||||
|
// API: Check answer and save result for the current user
|
||||||
|
app.post("/api/check-answer", requireAuth, (req, res) => {
|
||||||
const { taskId, answer } = req.body;
|
const { taskId, answer } = req.body;
|
||||||
|
|
||||||
if (!taskId || answer === undefined) {
|
if (!taskId || answer === undefined) {
|
||||||
return res.status(400).json({ error: 'taskId und answer sind erforderlich' });
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: "taskId und answer sind erforderlich" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const tasks = loadTasks();
|
const task = loadCaseTasks().find((t) => t.id === taskId);
|
||||||
const task = tasks.find(t => t.id === taskId);
|
|
||||||
|
|
||||||
if (!task) {
|
if (!task) {
|
||||||
return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
|
return res.status(404).json({ error: "Aufgabe nicht gefunden" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const userAnswer = parseInt(answer);
|
const userAnswer = parseInt(answer);
|
||||||
const correctAnswer = parseInt(task.answer);
|
const correctAnswer = parseInt(task.answer);
|
||||||
const isCorrect = userAnswer === correctAnswer;
|
const isCorrect = userAnswer === correctAnswer;
|
||||||
|
|
||||||
// Initialisiere attempts Counter falls nicht vorhanden
|
const progress = loadProgress(req.userId);
|
||||||
if (task.attempts === undefined) {
|
progress[taskId] = {
|
||||||
task.attempts = 0;
|
attempts: (progress[taskId]?.attempts || 0) + 1,
|
||||||
}
|
userAnswer,
|
||||||
|
isCorrect,
|
||||||
// Erhöhe Versuchszähler bei jeder Antwortprüfung
|
answerTimestamp: new Date().toISOString(),
|
||||||
task.attempts += 1;
|
};
|
||||||
|
saveProgress(req.userId, progress);
|
||||||
// Speichere Antwort, isCorrect und Timestamp in der Task
|
|
||||||
task.userAnswer = userAnswer;
|
|
||||||
task.isCorrect = isCorrect;
|
|
||||||
task.answerTimestamp = new Date().toISOString();
|
|
||||||
|
|
||||||
// Speichere die aktualisierte Task in der entsprechenden Datei
|
|
||||||
if (!saveTask(taskId, task)) {
|
|
||||||
return res.status(500).json({ error: 'Fehler beim Speichern der Antwort' });
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
correct: isCorrect,
|
correct: isCorrect,
|
||||||
correctAnswer: correctAnswer,
|
correctAnswer,
|
||||||
points: isCorrect ? task.points : 0
|
points: isCorrect ? task.points : 0,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Starte Server
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`MathQuest Server läuft auf http://localhost:${PORT}`);
|
console.log(`MathQuest Server läuft auf http://localhost:${PORT}`);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user