implement supabase

This commit is contained in:
Lukas Cremer
2026-05-13 21:32:49 +02:00
parent a49130973a
commit 56f41d9f8d
6 changed files with 528 additions and 411 deletions

View File

@@ -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();
}
});

View File

@@ -7,18 +7,52 @@
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="app-wrapper">
<aside class="sidebar" id="sidebar">
<!-- Sidebar wird hier dynamisch eingefügt -->
</aside>
<!-- Auth overlay — shown when not logged in -->
<div id="authOverlay" class="auth-overlay">
<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="container">
<header>
<div class="header-content">
<h1>Kriminalfälle - Mathematische Ermittlungen</h1>
<div class="points-display">
<div class="points-label">Ermittlungspunkte</div>
<div class="points-value" id="totalPoints">0</div>
<div class="header-right">
<div class="points-display">
<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>
</header>
@@ -35,9 +69,7 @@
<div class="task-section">
<div id="chapterDescription" class="chapter-description"></div>
<div id="taskContainer" class="task-container">
<!-- Aufgaben werden hier dynamisch eingefügt -->
</div>
<div id="taskContainer" class="task-container"></div>
</div>
<div class="message" id="message"></div>
@@ -45,6 +77,8 @@
</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>
</body>
</html>

View File

@@ -405,6 +405,157 @@ h1 {
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) {
.app-wrapper {
flex-direction: column;