-

Kriminalfälle - Mathematische Ermittlungen

+

Mathquest

-
Ermittlungspunkte
+
Punkte
0
@@ -100,7 +128,7 @@
-
Ermittlungsfortschritt
+
Fortschritt
diff --git a/public/style.css b/public/style.css index 809b0d9..8fe1689 100644 --- a/public/style.css +++ b/public/style.css @@ -1,3 +1,15 @@ +:root { + --theme-bg: #1a252f; + --theme-bg-mid: #2c3e50; + --theme-bg-light: #34495e; + --theme-bg-hover: #3d566e; + --theme-text: #ecf0f1; + --theme-muted: #7f8c8d; + --theme-muted-light: #bdc3c7; + --theme-accent: #3498db; + --theme-accent-hover: #2980b9; +} + * { margin: 0; padding: 0; @@ -6,7 +18,7 @@ body { font-family: 'Arial', 'Helvetica', sans-serif; - background: #2c3e50; + background: var(--theme-bg-mid); min-height: 100vh; padding: 0; margin: 0; @@ -22,8 +34,8 @@ body { .sidebar { width: 280px; - background: #34495e; - border-right: 3px solid #1a252f; + background: var(--theme-bg-light); + border-right: 3px solid var(--theme-bg); padding: 0; position: sticky; top: 0; @@ -33,13 +45,13 @@ body { } .sidebar-header { - background: #1a252f; + background: var(--theme-bg); padding: 20px; - border-bottom: 2px solid #0f1419; + border-bottom: 2px solid rgba(0, 0, 0, 0.3); } .sidebar-header h2 { - color: #ecf0f1; + color: var(--theme-text); font-size: 1.2em; font-weight: bold; text-transform: uppercase; @@ -50,19 +62,19 @@ body { padding: 15px 20px; cursor: pointer; transition: background-color 0.2s; - border-bottom: 1px solid #2c3e50; - background: #34495e; - color: #ecf0f1; + border-bottom: 1px solid var(--theme-bg-mid); + background: var(--theme-bg-light); + color: var(--theme-text); } .sidebar-item:hover { - background: #3d566e; + background: var(--theme-bg-hover); } .sidebar-item.active { - background: #1a252f; - border-left: 4px solid #3498db; - color: #ecf0f1; + background: var(--theme-bg); + border-left: 4px solid var(--theme-accent); + color: var(--theme-text); font-weight: bold; } @@ -76,7 +88,7 @@ body { } .sidebar-item.completed.active { - background: #1a252f; + background: var(--theme-bg); border-left: 4px solid #27ae60; } @@ -85,6 +97,26 @@ body { line-height: 1.4; } +.reset-button { + width: 100%; + padding: 8px 12px; + background: none; + border: 1px solid #7f3030; + color: #c0392b; + font-size: 0.8em; + font-family: 'Arial', sans-serif; + text-transform: uppercase; + letter-spacing: 0.5px; + cursor: pointer; + transition: background-color 0.2s, color 0.2s; + display: block; +} + +.reset-button:hover { + background: #7f3030; + color: #fff; +} + .main-content { flex: 1; min-width: 0; @@ -100,10 +132,10 @@ body { } header { - background: #1a252f; + background: var(--theme-bg); padding: 25px 40px; - border-bottom: 4px solid #3498db; - color: #ecf0f1; + border-bottom: 4px solid var(--theme-accent); + color: var(--theme-text); } .header-content { @@ -116,7 +148,7 @@ header { h1 { font-size: 1.8em; - color: #ecf0f1; + color: var(--theme-text); margin: 0; font-weight: bold; text-transform: uppercase; @@ -124,16 +156,16 @@ h1 { } .points-display { - background: #2c3e50; + background: var(--theme-bg-mid); padding: 15px 25px; - border: 2px solid #3498db; + border: 2px solid var(--theme-accent); border-radius: 4px; text-align: center; } .points-label { font-size: 0.85em; - color: #bdc3c7; + color: var(--theme-muted-light); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 5px; @@ -142,13 +174,13 @@ h1 { .points-value { font-size: 2em; font-weight: bold; - color: #3498db; + color: var(--theme-accent); } .progress-section { - background: #34495e; + background: var(--theme-bg-light); padding: 20px 40px; - border-bottom: 2px solid #2c3e50; + border-bottom: 2px solid var(--theme-bg-mid); } .progress-content { @@ -158,7 +190,7 @@ h1 { .progress-label { font-size: 0.9em; - color: #ecf0f1; + color: var(--theme-text); margin-bottom: 10px; font-weight: bold; text-transform: uppercase; @@ -168,22 +200,22 @@ h1 { .progress-bar-container { width: 100%; height: 25px; - background: #2c3e50; - border: 1px solid #1a252f; + background: var(--theme-bg-mid); + border: 1px solid var(--theme-bg); overflow: hidden; margin-bottom: 8px; } .progress-bar { height: 100%; - background: #3498db; + background: var(--theme-accent); width: 0%; transition: width 0.5s ease; } .progress-text { font-size: 0.9em; - color: #bdc3c7; + color: var(--theme-muted-light); font-weight: bold; } @@ -200,7 +232,7 @@ h1 { font-size: 1em; line-height: 1.7; color: #2c3e50; - border-left: 4px solid #3498db; + border-left: 4px solid var(--theme-accent); border-top: 1px solid #e0e0e0; border-right: 1px solid #e0e0e0; border-bottom: 1px solid #e0e0e0; @@ -216,14 +248,14 @@ h1 { background: #ffffff; padding: 30px; border: 2px solid #e0e0e0; - border-left: 4px solid #3498db; + border-left: 4px solid var(--theme-accent); transition: border-color 0.2s, box-shadow 0.2s; position: relative; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); } .task-card:hover { - border-color: #3498db; + border-color: var(--theme-accent); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } @@ -232,20 +264,15 @@ h1 { } @keyframes confettiFadeOut { - 0% { - opacity: 1; - } - 100% { - opacity: 0; - transform: translateY(-20px); - } + 0% { opacity: 1; } + 100% { opacity: 0; transform: translateY(-20px); } } .confetti { position: absolute; width: 8px; height: 8px; - background: #3498db; + background: var(--theme-accent); animation: confettiFall linear forwards; z-index: 1000; pointer-events: none; @@ -274,19 +301,19 @@ h1 { .task-title { font-size: 1.1em; font-weight: bold; - color: #1a252f; + color: var(--theme-bg); text-transform: uppercase; letter-spacing: 1px; } .task-points { - background: #34495e; - color: #ecf0f1; + background: var(--theme-bg-light); + color: var(--theme-text); padding: 6px 15px; border-radius: 2px; font-weight: bold; font-size: 0.9em; - border: 1px solid #2c3e50; + border: 1px solid var(--theme-bg-mid); } .task-question { @@ -323,12 +350,12 @@ h1 { .task-input:focus { outline: none; - border-color: #3498db; + border-color: var(--theme-accent); box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1); } .task-button { - background: #3498db; + background: var(--theme-accent); color: white; border: none; padding: 12px 30px; @@ -343,7 +370,7 @@ h1 { } .task-button:hover { - background: #2980b9; + background: var(--theme-accent-hover); } .task-button:active { @@ -362,17 +389,9 @@ h1 { min-height: 25px; } -.task-status.success { - color: #27ae60; -} - -.task-status.error { - color: #e74c3c; -} - -.task-status.completed { - color: #f39c12; -} +.task-status.success { color: #27ae60; } +.task-status.error { color: #e74c3c; } +.task-status.completed { color: #f39c12; } .message { position: fixed; @@ -389,9 +408,7 @@ h1 { border: 2px solid; } -.message.show { - transform: translateX(0); -} +.message.show { transform: translateX(0); } .message.success { background: #27ae60; @@ -410,7 +427,7 @@ h1 { .auth-overlay { position: fixed; inset: 0; - background: #1a252f; + background: var(--theme-bg); display: flex; align-items: center; justify-content: center; @@ -418,23 +435,30 @@ h1 { } .auth-card { - background: #2c3e50; - border: 1px solid #34495e; + background: var(--theme-bg-mid); + border: 1px solid var(--theme-bg-light); padding: 32px; width: 100%; max-width: 360px; } +.auth-title { + color: var(--theme-text); + font-size: 1.4em; + font-weight: bold; + margin-bottom: 20px; +} + .auth-tabs { display: flex; margin-bottom: 20px; - border-bottom: 1px solid #34495e; + border-bottom: 1px solid var(--theme-bg-light); } .auth-tab { background: none; border: none; - color: #7f8c8d; + color: var(--theme-muted); padding: 8px 16px; font-size: 0.9em; cursor: pointer; @@ -444,8 +468,8 @@ h1 { } .auth-tab.active { - color: #ecf0f1; - border-bottom-color: #3498db; + color: var(--theme-text); + border-bottom-color: var(--theme-accent); } .auth-input { @@ -453,9 +477,9 @@ h1 { width: 100%; padding: 10px 12px; margin-bottom: 10px; - background: #1a252f; - border: 1px solid #34495e; - color: #ecf0f1; + background: var(--theme-bg); + border: 1px solid var(--theme-bg-light); + color: var(--theme-text); font-size: 0.95em; transition: border-color 0.2s; box-sizing: border-box; @@ -463,11 +487,11 @@ h1 { .auth-input:focus { outline: none; - border-color: #3498db; + border-color: var(--theme-accent); } .auth-input::placeholder { - color: #7f8c8d; + color: var(--theme-muted); } .auth-error { @@ -480,7 +504,7 @@ h1 { .auth-button { width: 100%; padding: 11px; - background: #3498db; + background: var(--theme-accent); color: white; border: none; font-size: 0.95em; @@ -489,7 +513,88 @@ h1 { } .auth-button:hover { - background: #2980b9; + background: var(--theme-accent-hover); +} + +/* ─── Class picker & completion ──────────────────────────────────────────────── */ + +.class-picker-overlay { + position: fixed; + inset: 0; + background: var(--theme-bg); + display: flex; + align-items: center; + justify-content: center; + z-index: 9998; + padding: 20px; +} + +.class-picker-card { + background: var(--theme-bg-mid); + border: 1px solid var(--theme-bg-light); + padding: 40px; + width: 100%; + max-width: 560px; +} + +.class-picker-title { + color: var(--theme-text); + font-size: 1.4em; + font-weight: bold; + margin-bottom: 8px; +} + +.class-picker-subtitle { + color: var(--theme-muted); + font-size: 0.9em; + margin-bottom: 28px; +} + +.class-list { + display: grid; + gap: 12px; +} + +.class-card { + background: var(--theme-bg); + border: 2px solid var(--card-accent, var(--theme-accent)); + padding: 20px 24px; + cursor: pointer; + transition: opacity 0.15s, transform 0.15s; + display: flex; + align-items: center; + gap: 16px; +} + +.class-card:hover { + opacity: 0.85; + transform: translateY(-2px); +} + +.class-card-icon { + font-size: 2.2em; + flex-shrink: 0; +} + +.class-card-info { + flex: 1; +} + +.class-card-name { + color: var(--theme-text); + font-size: 1.1em; + font-weight: bold; + margin-bottom: 4px; +} + +.class-card-desc { + color: var(--theme-muted); + font-size: 0.85em; +} + +.completion-icon { + font-size: 3em; + margin-bottom: 16px; } /* ─── User display in header ─────────────────────────────────────────────── */ @@ -508,7 +613,7 @@ h1 { } .user-name { - color: #bdc3c7; + color: var(--theme-muted-light); font-size: 0.85em; text-transform: uppercase; letter-spacing: 1px; @@ -516,8 +621,8 @@ h1 { .logout-button { background: none; - border: 1px solid #34495e; - color: #7f8c8d; + border: 1px solid var(--theme-bg-light); + color: var(--theme-muted); padding: 5px 12px; font-size: 0.8em; font-family: 'Arial', sans-serif; @@ -536,20 +641,20 @@ h1 { .app-wrapper { flex-direction: column; } - + .sidebar { width: 100%; position: relative; height: auto; max-height: 300px; border-right: none; - border-bottom: 3px solid #1a252f; + border-bottom: 3px solid var(--theme-bg); } - + .sidebar-item { - border-bottom: 1px solid #2c3e50; + border-bottom: 1px solid var(--theme-bg-mid); } - + .header-content { flex-direction: column; gap: 20px; @@ -558,44 +663,15 @@ h1 { } @media (max-width: 600px) { - .container { - padding: 0; - } - - header { - padding: 20px; - } - - h1 { - font-size: 1.4em; - } - - .points-value { - font-size: 1.5em; - } - - .task-section { - padding: 20px; - } - - .task-input-group { - flex-direction: column; - align-items: stretch; - } - - .task-input { - width: 100%; - } - - .task-button { - width: 100%; - } - - .sidebar-item { - padding: 12px 15px; - } - - .sidebar-item-title { - font-size: 0.9em; - } + .container { padding: 0; } + header { padding: 20px; } + h1 { font-size: 1.4em; } + .points-value { font-size: 1.5em; } + .task-section { padding: 20px; } + .task-input-group { flex-direction: column; align-items: stretch; } + .task-input { width: 100%; } + .task-button { width: 100%; } + .sidebar-item { padding: 12px 15px; } + .sidebar-item-title { font-size: 0.9em; } + .class-picker-card { padding: 24px; } } diff --git a/server.js b/server.js index 4f4ea17..65acf81 100644 --- a/server.js +++ b/server.js @@ -6,19 +6,17 @@ const path = require("path"); const app = express(); const PORT = process.env.PORT || 3000; -const CASES_DIR = path.join(__dirname, "cases"); +const LESSONS_DIR = path.join(__dirname, "lessons"); +const CLASSES_DIR = path.join(__dirname, "classes"); const PROGRESS_DIR = path.join(__dirname, "data", "progress"); -// Ensure progress directory exists if (!fs.existsSync(PROGRESS_DIR)) { fs.mkdirSync(PROGRESS_DIR, { recursive: true }); } -// Middleware app.use(cors()); app.use(express.json()); -// Expose public Supabase config to the frontend app.get("/config.js", (_req, res) => { res.type("application/javascript"); res.send( @@ -28,7 +26,6 @@ app.get("/config.js", (_req, res) => { app.use(express.static("public")); -// Verify token by asking Supabase directly — works regardless of JWT algorithm async function requireAuth(req, res, next) { const auth = req.headers.authorization; if (!auth || !auth.startsWith("Bearer ")) { @@ -37,21 +34,14 @@ async function requireAuth(req, res, next) { try { const response = await fetch(`${process.env.SUPABASE_URL}/auth/v1/user`, { - headers: { - Authorization: auth, - apikey: process.env.SUPABASE_ANON_KEY, - }, + 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." }); + 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) { @@ -60,43 +50,52 @@ async function requireAuth(req, res, next) { } } -// 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) => { +function loadClasses() { + if (!fs.existsSync(CLASSES_DIR)) return {}; + const classes = {}; + fs.readdirSync(CLASSES_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, - }); - }, - ); - } + const cls = JSON.parse(fs.readFileSync(path.join(CLASSES_DIR, file), "utf8")); + classes[cls.id] = cls; } catch (err) { - console.error("Fehler beim Laden von", file, ":", err.message); + console.error("Fehler beim Laden von Klasse", file, ":", err.message); } }); + return classes; +} +function loadLessonsForClass(classId) { + const classes = loadClasses(); + const cls = classes[classId]; + if (!cls) return []; + + const allTasks = []; + (cls.lessons || []).forEach((lessonId) => { + const file = path.join(LESSONS_DIR, `${lessonId}.json`); + if (!fs.existsSync(file)) return; + try { + const lessonData = JSON.parse(fs.readFileSync(file, "utf8")); + if (Array.isArray(lessonData)) { + lessonData.forEach(({ userAnswer, isCorrect, attempts, answerTimestamp, ...task }) => { + allTasks.push(task); + }); + } else if (lessonData.tasks) { + lessonData.tasks.forEach(({ userAnswer, isCorrect, attempts, answerTimestamp, ...task }) => { + allTasks.push({ + ...task, + chapter: lessonData.chapter, + chapterDescription: lessonData.description, + }); + }); + } + } catch (err) { + console.error("Fehler beim Laden von Lektion", lessonId, ":", 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 {}; @@ -114,24 +113,100 @@ function saveProgress(userId, progress) { ); } -// API: Get all tasks merged with user's progress -app.get("/api/tasks", requireAuth, (req, res) => { - const tasks = loadCaseTasks(); +// GET /api/classes — list available classes +app.get("/api/classes", requireAuth, (_req, res) => { + const classes = loadClasses(); + res.json({ + classes: Object.values(classes).map(({ id, name, icon, description, theme }) => ({ + id, name, icon, description, theme, + })), + }); +}); + +// GET /api/me — user's active class and completed classes +app.get("/api/me", requireAuth, (req, res) => { const progress = loadProgress(req.userId); + const meta = progress._meta || { activeClass: null, completedClasses: [] }; + res.json(meta); +}); + +// POST /api/set-class — set user's active class +app.post("/api/set-class", requireAuth, (req, res) => { + const { classId } = req.body; + const classes = loadClasses(); + + if (!classId || !classes[classId]) { + return res.status(400).json({ error: "Unbekannte Klasse" }); + } + + const progress = loadProgress(req.userId); + if (!progress._meta) progress._meta = { activeClass: null, completedClasses: [] }; + progress._meta.activeClass = classId; + saveProgress(req.userId, progress); + + const { id, name, icon, theme } = classes[classId]; + res.json({ ok: true, class: { id, name, icon, theme } }); +}); + +// POST /api/complete-class — mark active class as done and clear it +app.post("/api/complete-class", requireAuth, (req, res) => { + const progress = loadProgress(req.userId); + if (!progress._meta) progress._meta = { activeClass: null, completedClasses: [] }; + + const { activeClass } = progress._meta; + if (activeClass && !progress._meta.completedClasses.includes(activeClass)) { + progress._meta.completedClasses.push(activeClass); + } + progress._meta.activeClass = null; + saveProgress(req.userId, progress); + res.json({ ok: true }); +}); + +// POST /api/reset-class-progress — delete all task progress for the active class +app.post("/api/reset-class-progress", requireAuth, (req, res) => { + const progress = loadProgress(req.userId); + const activeClass = progress._meta?.activeClass; + + if (!activeClass) { + return res.status(400).json({ error: "Keine Klasse ausgewählt" }); + } + + const taskIds = new Set(loadLessonsForClass(activeClass).map((t) => t.id)); + Object.keys(progress).forEach((key) => { + if (taskIds.has(key)) delete progress[key]; + }); + saveProgress(req.userId, progress); + res.json({ ok: true }); +}); + +// GET /api/tasks — tasks for user's active class +app.get("/api/tasks", requireAuth, (req, res) => { + const progress = loadProgress(req.userId); + const activeClass = progress._meta?.activeClass; + + if (!activeClass) { + return res.status(400).json({ error: "Keine Klasse ausgewählt", noClass: true }); + } + + const tasks = loadLessonsForClass(activeClass); res.json({ tasks: tasks.map((task) => ({ ...task, ...progress[task.id] })) }); }); -// API: Check answer and save result for the current user +// POST /api/check-answer app.post("/api/check-answer", requireAuth, (req, res) => { const { taskId, answer } = req.body; 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 task = loadCaseTasks().find((t) => t.id === taskId); + const progress = loadProgress(req.userId); + const activeClass = progress._meta?.activeClass; + if (!activeClass) { + return res.status(400).json({ error: "Keine Klasse ausgewählt" }); + } + + const task = loadLessonsForClass(activeClass).find((t) => t.id === taskId); if (!task) { return res.status(404).json({ error: "Aufgabe nicht gefunden" }); } @@ -140,7 +215,6 @@ app.post("/api/check-answer", requireAuth, (req, res) => { const correctAnswer = parseInt(task.answer); const isCorrect = userAnswer === correctAnswer; - const progress = loadProgress(req.userId); progress[taskId] = { attempts: (progress[taskId]?.attempts || 0) + 1, userAnswer, @@ -149,11 +223,7 @@ app.post("/api/check-answer", requireAuth, (req, res) => { }; saveProgress(req.userId, progress); - res.json({ - correct: isCorrect, - correctAnswer, - points: isCorrect ? task.points : 0, - }); + res.json({ correct: isCorrect, correctAnswer, points: isCorrect ? task.points : 0 }); }); app.listen(PORT, () => {