implement supabase
This commit is contained in:
426
public/app.js
426
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 = '<h2>Aktenübersicht</h2>';
|
||||
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 = `<div class="sidebar-item-title">${isCompleted ? '✓ ' : ''}${chapter}</div>`;
|
||||
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 = `
|
||||
<div class="task-header">
|
||||
<div class="task-title">Ermittlungsaufgabe</div>
|
||||
<div class="task-points">${task.points} EP</div>
|
||||
</div>
|
||||
<div class="task-question">${task.question}</div>
|
||||
<div class="task-input-group">
|
||||
<input
|
||||
type="number"
|
||||
class="task-input"
|
||||
id="input-${task.id}"
|
||||
placeholder="?"
|
||||
value="${hasAnswer ? task.userAnswer : ''}"
|
||||
>
|
||||
<button
|
||||
class="task-button"
|
||||
onclick="checkAnswer('${task.id}')"
|
||||
>
|
||||
Prüfen
|
||||
</button>
|
||||
<input type="number" class="task-input" id="input-${task.id}"
|
||||
placeholder="?" value="${hasAnswer ? task.userAnswer : ''}">
|
||||
<button class="task-button" onclick="checkAnswer('${task.id}')">Prüfen</button>
|
||||
</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.' : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user