// ─── Supabase config (injected by server from .env via /config.js) ─────────── const supabaseClient = supabase.createClient(window.SUPABASE_URL, window.SUPABASE_ANON_KEY); // ─── App state ─────────────────────────────────────────────────────────────── let totalPoints = 0; let tasks = []; let chapters = []; let chapterDescriptions = {}; let activeChapter = null; // ─── 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(); renderSidebar(); renderTasks(); updateProgress(); } 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) .reduce((sum, task) => sum + (task.points || 0), 0); updatePointsDisplay(); } function isChapterCompleted(chapter) { const chapterTasks = tasks.filter(task => task.chapter === chapter); return chapterTasks.length > 0 && chapterTasks.every(task => task.isCorrect === true); } function renderSidebar() { const sidebar = document.getElementById('sidebar'); sidebar.innerHTML = ''; if (chapters.length === 0) { sidebar.style.display = 'none'; return; } sidebar.style.display = 'block'; const header = document.createElement('div'); header.className = 'sidebar-header'; header.innerHTML = '

Aktenübersicht

'; sidebar.appendChild(header); chapters.forEach(chapter => { const isCompleted = isChapterCompleted(chapter); const item = document.createElement('div'); item.className = `sidebar-item ${activeChapter === chapter ? 'active' : ''} ${isCompleted ? 'completed' : ''}`; item.onclick = () => switchChapter(chapter); item.innerHTML = ``; sidebar.appendChild(item); }); } function switchChapter(chapter) { activeChapter = chapter; renderSidebar(); renderTasks(); } function renderTasks() { const container = document.getElementById('taskContainer'); const descriptionEl = document.getElementById('chapterDescription'); container.innerHTML = ''; if (activeChapter && chapterDescriptions[activeChapter]) { descriptionEl.textContent = chapterDescriptions[activeChapter]; descriptionEl.style.display = 'block'; } else { descriptionEl.style.display = 'none'; } const visibleTasks = (activeChapter ? tasks.filter(t => t.chapter === activeChapter) : tasks) .filter(task => task.isCorrect !== true); visibleTasks.forEach(task => { const hasAnswer = task.userAnswer !== undefined; const card = document.createElement('div'); card.className = 'task-card'; card.id = `task-${task.id}`; card.innerHTML = `
Ermittlungsaufgabe
${task.points} EP
${task.question}
${hasAnswer && !task.isCorrect ? '✗ Berechnung fehlerhaft. Bitte erneut prüfen.' : ''}
`; container.appendChild(card); }); } async function checkAnswer(taskId) { const task = tasks.find(t => t.id === taskId); if (!task || task.isCorrect === true) return; const input = document.getElementById(`input-${taskId}`); const status = document.getElementById(`status-${taskId}`); const userAnswer = parseInt(input.value); if (isNaN(userAnswer)) { status.textContent = 'Ungültige Eingabe. Bitte eine Zahl eingeben.'; status.className = 'task-status error'; return; } const res = await apiFetch('/api/check-answer', { method: 'POST', body: JSON.stringify({ taskId, answer: userAnswer }), }); if (!res) return; const data = await res.json(); 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'); const taskCard = document.getElementById(`task-${taskId}`); if (taskCard) { createConfetti(taskCard); taskCard.classList.add('confetti-animation'); setTimeout(() => { renderSidebar(); renderTasks(); updateProgress(); }, 1500); } } else { status.textContent = '✗ Berechnung fehlerhaft. Bitte erneut prüfen.'; status.className = 'task-status error'; input.focus(); renderSidebar(); renderTasks(); updateProgress(); } } // ─── UI helpers ─────────────────────────────────────────────────────────────── function updatePointsDisplay() { document.getElementById('totalPoints').textContent = totalPoints; } function updateProgress() { const maxPoints = tasks.reduce((sum, task) => sum + (task.points || 0), 0); const percentage = maxPoints > 0 ? Math.min((totalPoints / maxPoints) * 100, 100) : 0; document.getElementById('progressBar').style.width = `${percentage}%`; document.getElementById('progressText').textContent = `${totalPoints} / ${maxPoints}`; } function createConfetti(element) { const colors = ['#3498db', '#2980b9', '#34495e', '#2c3e50', '#1a252f', '#27ae60', '#16a085', '#7f8c8d']; const rect = element.getBoundingClientRect(); for (let i = 0; i < 50; i++) { const confetti = document.createElement('div'); confetti.className = 'confetti'; 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)]; confetti.style.width = (Math.random() * 8 + 6) + 'px'; confetti.style.height = (Math.random() * 8 + 6) + 'px'; confetti.style.animationDuration = (Math.random() * 0.8 + 0.7) + 's'; confetti.style.animationDelay = Math.random() * 0.3 + 's'; confetti.style.borderRadius = Math.random() > 0.5 ? '50%' : '0%'; confetti.style.setProperty('--random-x', randomX); confetti.style.position = 'fixed'; document.body.appendChild(confetti); setTimeout(() => confetti.remove(), 2000); } } function showMessage(text, type) { const message = document.getElementById('message'); message.textContent = text; message.className = `message ${type} show`; setTimeout(() => message.classList.remove('show'), 3000); } // ─── 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(); } });