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; let activeClassData = 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) { errorEl.style.color = '#27ae60'; errorEl.textContent = 'Konto erstellt! Bitte E-Mail bestätigen, dann anmelden.'; } } 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; } // ─── Theme ─────────────────────────────────────────────────────────────────── function applyTheme(themeName) { let link = document.getElementById('class-theme'); if (!link) { link = document.createElement('link'); link.id = 'class-theme'; link.rel = 'stylesheet'; document.head.appendChild(link); } link.href = `/themes/${encodeURIComponent(themeName)}.css`; } // ─── Screen management ─────────────────────────────────────────────────────── function showScreen(screen) { document.getElementById('authOverlay').style.display = 'none'; document.getElementById('classPickerOverlay').style.display = 'none'; document.getElementById('classCompletionOverlay').style.display = 'none'; document.getElementById('appWrapper').style.display = 'none'; if (screen === 'auth') { document.getElementById('authOverlay').style.display = 'flex'; } else if (screen === 'class-picker') { document.getElementById('classPickerOverlay').style.display = 'flex'; } else if (screen === 'class-completion') { document.getElementById('classCompletionOverlay').style.display = 'flex'; } else if (screen === 'app') { document.getElementById('appWrapper').style.display = 'flex'; } } // ─── Class picker ──────────────────────────────────────────────────────────── async function showClassPicker() { showScreen('class-picker'); const listEl = document.getElementById('classList'); listEl.innerHTML = '

Laden...

'; const res = await apiFetch('/api/classes'); if (!res) return; const { classes } = await res.json(); listEl.innerHTML = ''; if (classes.length === 0) { listEl.innerHTML = '

Keine Klassen verfügbar.

'; return; } classes.forEach(cls => { const card = document.createElement('div'); card.className = 'class-card'; const c = cls.colors || {}; if (c.bg) card.style.background = c.bg; if (c.accent) card.style.borderColor = c.accent; card.innerHTML = `
${cls.icon || '📚'}
${cls.name}
${cls.description || ''}
`; card.onclick = () => selectClass(cls.id); listEl.appendChild(card); }); } async function selectClass(classId) { const res = await apiFetch('/api/set-class', { method: 'POST', body: JSON.stringify({ classId }), }); if (!res || !res.ok) return; const data = await res.json(); activeClassData = data.class; applyTheme(activeClassData.theme); const h1 = document.querySelector('header h1'); if (h1) h1.textContent = activeClassData.name; activeChapter = null; showScreen('app'); await loadAndRenderApp(); } async function completeAndPickClass() { await apiFetch('/api/complete-class', { method: 'POST' }); activeClassData = null; activeChapter = null; tasks = []; await showClassPicker(); } // ─── App init ──────────────────────────────────────────────────────────────── async function initApp(user) { const displayName = user.user_metadata?.display_name || user.email; document.getElementById('userDisplayName').textContent = displayName; const meRes = await apiFetch('/api/me'); if (!meRes) return; const meta = await meRes.json(); if (!meta.activeClass) { await showClassPicker(); return; } const classesRes = await apiFetch('/api/classes'); if (classesRes) { const { classes } = await classesRes.json(); activeClassData = classes.find(c => c.id === meta.activeClass) || null; if (activeClassData) { applyTheme(activeClassData.theme); const h1 = document.querySelector('header h1'); if (h1) h1.textContent = activeClassData.name; } } showScreen('app'); await loadAndRenderApp(); } async function loadAndRenderApp() { await loadTasks(); extractChapters(); calculateTotalPoints(); renderSidebar(); renderTasks(); updateProgress(); checkClassCompletion(); } supabaseClient.auth.onAuthStateChange((_event, session) => { if (session?.user) { initApp(session.user); } else { showScreen('auth'); } }); // ─── 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 checkClassCompletion() { if (tasks.length > 0 && tasks.every(t => t.isCorrect === true)) { const maxPoints = tasks.reduce((sum, task) => sum + (task.points || 0), 0); document.getElementById('completionPoints').textContent = maxPoints; showScreen('class-completion'); } } async function resetClassProgress(btn) { if (!btn.dataset.confirmed) { btn.dataset.confirmed = '1'; btn.textContent = 'Wirklich löschen? Nochmal klicken.'; btn.style.background = '#7f3030'; btn.style.color = '#fff'; setTimeout(() => { delete btn.dataset.confirmed; btn.textContent = 'Fortschritt zurücksetzen'; btn.style.background = ''; btn.style.color = ''; }, 3000); return; } const res = await apiFetch('/api/reset-class-progress', { method: 'POST' }); if (!res || !res.ok) return; activeChapter = null; await loadTasks(); extractChapters(); calculateTotalPoints(); showScreen('app'); renderSidebar(); renderTasks(); updateProgress(); } 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 = '

Lektionen

'; 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 = `
Aufgabe
${task.points} Punkte
${task.question}
${hasAnswer && !task.isCorrect ? '✗ Falsche Antwort. Bitte erneut versuchen.' : ''}
`; 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 = '✓ Richtig!'; status.className = 'task-status success'; showMessage(`✓ ${data.points} Punkte erhalten`, 'success'); const taskCard = document.getElementById(`task-${taskId}`); if (taskCard) { createConfetti(taskCard); taskCard.classList.add('confetti-animation'); setTimeout(() => { renderSidebar(); renderTasks(); updateProgress(); checkClassCompletion(); }, 1500); } } else { status.textContent = '✗ Falsche Antwort. Bitte erneut versuchen.'; 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(); } });