add diffent lessions
This commit is contained in:
18
classes/grundrechenarten.json
Normal file
18
classes/grundrechenarten.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "grundrechenarten",
|
||||
"name": "Grundrechenarten",
|
||||
"icon": "➕",
|
||||
"description": "Lerne die Grundlagen der Mathematik",
|
||||
"theme": {
|
||||
"--theme-bg": "#1a2f1a",
|
||||
"--theme-bg-mid": "#2a4a2a",
|
||||
"--theme-bg-light": "#3a6a3a",
|
||||
"--theme-bg-hover": "#4a7a4a",
|
||||
"--theme-text": "#e8f5e8",
|
||||
"--theme-muted": "#7fa87f",
|
||||
"--theme-muted-light": "#aed4ae",
|
||||
"--theme-accent": "#4caf50",
|
||||
"--theme-accent-hover": "#388e3c"
|
||||
},
|
||||
"lessons": ["adding"]
|
||||
}
|
||||
18
classes/kriminalfälle.json
Normal file
18
classes/kriminalfälle.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "kriminalfälle",
|
||||
"name": "Kriminalfälle",
|
||||
"icon": "🔍",
|
||||
"description": "Löse echte Kriminalfälle aus Deutschland mit Mathematik",
|
||||
"theme": {
|
||||
"--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"
|
||||
},
|
||||
"lessons": ["steglitz", "bode-museum", "dresden-gruenes-gewoelbe"]
|
||||
}
|
||||
146
lessons/adding.json
Normal file
146
lessons/adding.json
Normal file
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"chapter": "➕ Addieren",
|
||||
"description": "Lerne, wie man Zahlen addiert.",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "adding-1",
|
||||
"question": "Was ist 1 + 1?",
|
||||
"answer": 2,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "adding-2",
|
||||
"question": "Was ist 2 + 1?",
|
||||
"answer": 3,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "adding-3",
|
||||
"question": "Was ist 2 + 2?",
|
||||
"answer": 4,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "adding-4",
|
||||
"question": "Was ist 3 + 2?",
|
||||
"answer": 5,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "adding-5",
|
||||
"question": "Was ist 3 + 3?",
|
||||
"answer": 6,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "adding-6",
|
||||
"question": "Was ist 4 + 3?",
|
||||
"answer": 7,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "adding-7",
|
||||
"question": "Was ist 4 + 4?",
|
||||
"answer": 8,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "adding-8",
|
||||
"question": "Was ist 5 + 4?",
|
||||
"answer": 9,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "adding-9",
|
||||
"question": "Was ist 5 + 5?",
|
||||
"answer": 10,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "adding-10",
|
||||
"question": "Was ist 6 + 4?",
|
||||
"answer": 10,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "adding-11",
|
||||
"question": "Was ist 6 + 5?",
|
||||
"answer": 11,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "adding-12",
|
||||
"question": "Was ist 6 + 6?",
|
||||
"answer": 12,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "adding-13",
|
||||
"question": "Was ist 7 + 5?",
|
||||
"answer": 12,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "adding-14",
|
||||
"question": "Was ist 7 + 6?",
|
||||
"answer": 13,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "adding-15",
|
||||
"question": "Was ist 7 + 7?",
|
||||
"answer": 14,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "adding-16",
|
||||
"question": "Was ist 8 + 6?",
|
||||
"answer": 14,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "adding-17",
|
||||
"question": "Was ist 8 + 7?",
|
||||
"answer": 15,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "adding-18",
|
||||
"question": "Was ist 9 + 7?",
|
||||
"answer": 16,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "adding-19",
|
||||
"question": "Was ist 9 + 8?",
|
||||
"answer": 17,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "adding-20",
|
||||
"question": "Was ist 9 + 9?",
|
||||
"answer": 18,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
}
|
||||
]
|
||||
}
|
||||
57
lessons/bode-museum.json
Normal file
57
lessons/bode-museum.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"chapter": "🔍 Bode-Museum: Der Big Maple Leaf Raub",
|
||||
"description": "In der Nacht zum 27. März 2017 wurde aus dem Bode-Museum in Berlin die 100 kg schwere Goldmünze „Big Maple Leaf“ gestohlen. Drei Täter drangen während einer S-Bahn-Betriebspause über die Bahngleise ein, stiegen mit einer Leiter durch ein Fenster ein und zerschlugen die Panzerglasvitrine. Der Materialwert der Münze betrug etwa 3,75 Millionen Euro. Die Münze wurde vermutlich zersägt und eingeschmolzen; die Täter wurden später gefasst und verurteilt.",
|
||||
"data": {
|
||||
"gewicht_kg": { "value": 100, "description": "Gewicht der Goldmünze Big Maple Leaf in Kilogramm" },
|
||||
"materialwert_euro": { "value": 3750000, "description": "Materialwert der Münze zum Tatzeitpunkt in Euro (ca. 3,75 Mio €)" },
|
||||
"goldpreis_pro_kg_euro": { "value": 37500, "description": "Goldpreis pro Kilogramm zum Tatzeitpunkt in Euro" },
|
||||
"nennwert_dollar": { "value": 1000000, "description": "Nennwert der Münze in kanadischen Dollar" },
|
||||
"durchmesser_cm": { "value": 53, "description": "Durchmesser der Münze in Zentimetern" },
|
||||
"tatdatum": { "value": "2017-03-27", "description": "Datum des Diebstahls (27. März 2017)" },
|
||||
"tatzeit_von": { "value": "03:20", "description": "Beginn des Tatzeitfensters (Quelle: Polizei/Ermittlungen)" },
|
||||
"tatzeit_bis": { "value": "03:45", "description": "Ende des Tatzeitfensters (Quelle: Polizei/Ermittlungen)" },
|
||||
"tatzeitfenster_minuten": { "value": 25, "description": "Zeitfenster des Diebstahls in Minuten (03:20–03:45)" },
|
||||
"polizei_alarm_uhrzeit": { "value": "04:00", "description": "Uhrzeit der Alarmierung der Polizei (Quelle: Berichterstattung)" },
|
||||
"anzahl_taeter": { "value": 3, "description": "Anzahl der Täter (drei junge Männer)" },
|
||||
"entschaedigung_eigentuemer_euro": { "value": 2100000, "description": "Entschädigung an den privaten Eigentümer in Euro (Kammergericht)" },
|
||||
"entschaedigung_prozent": { "value": 50, "description": "Anteil des Versicherungswertes als Entschädigung in Prozent" },
|
||||
"versicherungswert_euro": { "value": 4200000, "description": "Versicherungswert in Euro (2,1 Mio € = 50 % davon)" },
|
||||
"wertersatz_gericht_euro": { "value": 3300000, "description": "Vom Gericht angeordneter Wertersatz durch die Verurteilten in Euro (gesamt)" },
|
||||
"haftstrafe_jahre_taeter_1_2": { "value": 4.5, "description": "Haftstrafe für zwei Täter in Jahren (viereinhalb Jahre)" },
|
||||
"haftstrafe_jahre_monate_taeter_3": { "value": 3.333, "description": "Haftstrafe für dritten Täter (3 Jahre und 4 Monate, gerundet)" },
|
||||
"timeline": [
|
||||
{ "date": "2017-03-27", "time": "03:20", "description": "Vermutlicher Beginn: Täter dringen über S-Bahngleise und Fenster ein (S-Bahn-Betriebspause)." },
|
||||
{ "date": "2017-03-27", "time": "03:20–03:45", "description": "Tatzeitfenster: Panzerglasvitrine zerschlagen, Münze (100 kg Big Maple Leaf) entwendet, Abtransport über Gleise." },
|
||||
{ "date": "2017-03-27", "time": "04:00", "description": "Polizei wird alarmiert (Quelle: Berichterstattung)." },
|
||||
{ "date": "2017-03-27", "description": "Tat entdeckt; Ermittlungen." },
|
||||
{ "date": "2017", "description": "Ermittlungen werden der Familie R. (Remmo) zugeordnet; vier Beschuldigte (u.a. zwei Cousins, Ex-Wachmann)." },
|
||||
{ "date": "2019", "description": "Verfahren vor dem Landgericht Berlin (Beginn etwa ein Jahr vor Urteil)." },
|
||||
{ "date": "2020-02-26", "description": "Urteil Landgericht Berlin: Wissam R. und Ahmad R. je 4,5 Jahre Jugendstrafe; Denis W. 3 Jahre und 4 Monate; Wayci R. Freispruch. Wertersatz 3,3 Mio € (Remmo-Cousins), Denis W. 100.000 €." },
|
||||
{ "date": "2021-06-24", "description": "Bundesgerichtshof weist Revision zurück; Urteile rechtskräftig." },
|
||||
{ "date": null, "description": "Kammergericht Berlin: Privateigentümer erhält 2,1 Mio € Entschädigung (50 % des Versicherungswertes) wegen Sicherheitsmängeln." }
|
||||
]
|
||||
},
|
||||
"tasks": [
|
||||
{
|
||||
"id": "bode-museum-1",
|
||||
"question": "📊 SCHADENSBERECHNUNG (Ermittler/Versicherung):\n\nDie gestohlene Goldmünze „Big Maple Leaf“ wiegt 100 kg. Der Goldpreis zum Tatzeitpunkt (27. März 2017) betrug 37.500 € pro Kilogramm.\n\nBerechne den materiellen Schaden in Euro, den die Ermittler für Anklage und Versicherung ansetzen.",
|
||||
"answer": 3750000,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "bode-museum-2",
|
||||
"question": "📋 VERSICHERUNG (Rückrechnung):\n\nDer Eigentümer der Münze erhielt 2,1 Millionen Euro Entschädigung. Das war die Hälfte (50 %) des Versicherungswertes.\n\nWie hoch war der volle Versicherungswert in Euro? (Die Hälfte mal 2 = Ganzes.)",
|
||||
"answer": 4200000,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "bode-museum-3",
|
||||
"question": "⏱ EINSATZAUSWERTUNG (Reaktionszeit):\n\nDer Diebstahl erfolgte zwischen 03:20 und 03:45 Uhr. Die Polizei wurde um 04:00 Uhr alarmiert.\n\nWie viele Minuten vergingen vom spätesten Tatende (03:45) bis zur Alarmierung? (Relevanz für die Bewertung der Reaktionszeit.)",
|
||||
"answer": 15,
|
||||
"points": 5,
|
||||
"type": "word-problem"
|
||||
}
|
||||
]
|
||||
}
|
||||
51
lessons/dresden-gruenes-gewoelbe.json
Normal file
51
lessons/dresden-gruenes-gewoelbe.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"chapter": "💎 Dresden: Der Juwelendiebstahl im Grünen Gewölbe",
|
||||
"description": "Am 25. November 2019 drangen Täter in den frühen Morgenstunden in das Historische Grüne Gewölbe im Dresdner Residenzschloss ein. Sie nutzten einen Stromausfall (Brand an einem Stromkasten unter der Augustusbrücke), stahlen aus dem Juwelenzimmer 21 Schmuckstücke mit insgesamt 4.300 Diamanten und Brillanten. Der Versicherungswert betrug mindestens 113,8 Millionen Euro. Sechs Mitglieder der Berliner Remmo-Großfamilie wurden angeklagt; im Dezember 2022 gab der Clan einen großen Teil der Objekte zurück – teilweise beschädigt. Fünf Täter wurden 2023 verurteilt, einer freigesprochen.",
|
||||
"data": {
|
||||
"tatdatum": { "value": "2019-11-25", "description": "Datum des Einbruchs (25. November 2019)" },
|
||||
"tatzeit": { "value": "ca. 05:00", "description": "Frühe Morgenstunden, vermutlich gegen 5 Uhr" },
|
||||
"entdeckung_uhrzeit": { "value": "04:55", "description": "Sicherheitsdienst bemerkte Täter kurz vor 5 Uhr auf den Kameras" },
|
||||
"polizei_alarm_uhrzeit": { "value": "05:00", "description": "Polizei wurde gegen 5 Uhr informiert" },
|
||||
"streife_eintreffen_minuten": { "value": 5, "description": "Erste Funkstreife traf etwa 5 Minuten nach Alarm am Tatort ein" },
|
||||
"schmuckstuecke_gestohlen": { "value": 21, "description": "Anzahl der gestohlenen Schmuckstücke" },
|
||||
"diamanten_gesamt": { "value": 4300, "description": "Anzahl der Diamanten/Brillanten in den gestohlenen Stücken" },
|
||||
"versicherungswert_euro": { "value": 113800000, "description": "Versicherungswert der gestohlenen Objekte in Euro (mind. 113,8 Mio €)" },
|
||||
"angeklagte": { "value": 6, "description": "Anzahl der Angeklagten (Remmo-Großfamilie)" },
|
||||
"verurteilt": { "value": 5, "description": "Anzahl der Verurteilten (Mai 2023)" },
|
||||
"freigesprochen": { "value": 1, "description": "Anzahl der Freigesprochenen" },
|
||||
"schaden_beschaedigung_euro": { "value": 23500000, "description": "Geschätzter Schaden durch Beschädigung bei Rückgabe (22–25 Mio €, Mittelwert 23,5 Mio)" },
|
||||
"prozessbeginn": { "value": "2022-01-28", "description": "Beginn des Prozesses vor dem Landgericht Dresden" },
|
||||
"rueckgabe": { "value": "2022-12", "description": "Rückgabe eines großen Teils der Objekte durch den Clan (beschädigt)" },
|
||||
"urteil": { "value": "2023-05", "description": "Verurteilung von fünf Tätern" },
|
||||
"timeline": [
|
||||
{ "date": "2019-11-25", "time": "ca. 05:00", "description": "Einbruch: Täter nutzen Stromausfall (Brand Augustusbrücke), dringen ins Juwelenzimmer ein, stehlen 21 Schmuckstücke mit 4.300 Diamanten (Versicherungswert mind. 113,8 Mio €)." },
|
||||
{ "date": "2022-01", "description": "Sechs Mitglieder der Berliner Remmo-Großfamilie festgenommen und angeklagt." },
|
||||
{ "date": "2022-01-28", "description": "Prozessbeginn vor dem Landgericht Dresden." },
|
||||
{ "date": "2022-12", "description": "Clan gibt großen Teil der gestohlenen Objekte zurück – teilweise beschädigt; Schadensschätzung 22–25 Mio €." },
|
||||
{ "date": "2023-05", "description": "Fünf Täter verurteilt, einer freigesprochen." }
|
||||
]
|
||||
},
|
||||
"tasks": [
|
||||
{
|
||||
"id": "dresden-1",
|
||||
"question": "📊 SCHADENSBERECHNUNG (Ermittler/Versicherung):\n\nDie gestohlenen 21 Schmuckstücke enthielten insgesamt 4.300 Diamanten und Brillanten. Der Versicherungswert betrug mindestens 113,8 Millionen Euro.\n\nWie viel Euro Versicherungswert entfielen durchschnittlich pro gestohlenes Schmuckstück? (Runde auf ganze Millionen Euro – gib die Zahl in Euro an, z. B. 5000000.)",
|
||||
"answer": 5000000,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "dresden-2",
|
||||
"question": "⏱ EINSATZAUSWERTUNG (Zeitfenster):\n\nDer Sicherheitsdienst bemerkte die Täter um 04:55 Uhr auf den Überwachungskameras im Juwelenzimmer. Die erste Polizeistreife traf um 05:05 Uhr am Tatort ein.\n\nWie viele Minuten Vorsprung hatten die Täter zwischen der Entdeckung und dem Eintreffen der Polizei? (Relevanz für die Flucht.)",
|
||||
"answer": 10,
|
||||
"points": 5,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "dresden-3",
|
||||
"question": "📐 DIAMANTEN PRO STÜCK (Ermittler-Kennzahl):\n\nEs wurden 21 Schmuckstücke mit insgesamt 4.300 Diamanten und Brillanten gestohlen.\n\nWie viele Diamanten/Brillanten entfielen durchschnittlich pro Schmuckstück? (Runde auf ganze Zahl.)",
|
||||
"answer": 205,
|
||||
"points": 10,
|
||||
"type": "word-problem"
|
||||
}
|
||||
]
|
||||
}
|
||||
54
lessons/steglitz.json
Normal file
54
lessons/steglitz.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"chapter": "🏦 Berlin-Steglitz: Der Tunnel-Bankraub 2013",
|
||||
"description": "In der Nacht zum 14. Januar 2013 drangen Täter in eine Filiale der Berliner Volksbank in Steglitz ein – über einen etwa 45 Meter langen Tunnel aus einer gemieteten Tiefgarage. Sie durchbrachen eine 80 cm dicke Stahlbetonwand zum Tresorraum und brachen 309 der rund 1600 Schließfächer auf (294 davon waren vermietet). Die Beute wurde auf etwa 10 Millionen Euro geschätzt. Die Täter legten ein Feuer zur Spurenvernichtung; die Bande wurde nie gefasst.",
|
||||
"data": {
|
||||
"tatdatum": { "value": "2013-01-14", "description": "Datum des Einbruchs (Nacht zum 14. Januar 2013)" },
|
||||
"tatzeit_hinweis": { "value": "Nacht zum 14.01., keine exakten Uhrzeiten öffentlich", "description": "Zum Einbruch selbst sind öffentlich keine genauen Uhrzeiten überliefert; nur die Entdeckungszeit ist bekannt." },
|
||||
"entdeckung_uhrzeit": { "value": "06:15", "description": "Entdeckung am Montagmorgen gegen 6:15 Uhr – Anwohner bemerkt Rauch in der Tiefgarage (Feuer zur Spurenvernichtung)." },
|
||||
"bank": { "value": "Berliner Volksbank", "description": "Betroffene Bank (Filiale Steglitz)" },
|
||||
"tunnel_laenge_m": { "value": 45, "description": "Länge des Tunnels von der Tiefgarage zum Tresorraum in Metern" },
|
||||
"tunnel_tiefe_m": { "value": 4, "description": "Tiefe des Tunnels unter der Erde in Metern" },
|
||||
"wand_tresor_cm": { "value": 80, "description": "Dicke der durchbrochenen Stahlbetonwand zum Tresorraum in Zentimetern" },
|
||||
"wand_tiefgarage_cm": { "value": 30, "description": "Dicke der durchbrochenen Betonwand der Tiefgarage in Zentimetern" },
|
||||
"schliessfaecher_gesamt": { "value": 1600, "description": "Anzahl der Schließfächer im Tresorraum insgesamt (rund)" },
|
||||
"schliessfaecher_aufgebrochen": { "value": 309, "description": "Anzahl der von den Tätern aufgebrochenen Schließfächer" },
|
||||
"schliessfaecher_vermietet": { "value": 294, "description": "Anzahl der vermieteten unter den aufgebrochenen Fächern" },
|
||||
"schliessfaecher_leer": { "value": 15, "description": "Anzahl der aufgebrochenen, aber nicht vermieteten (leeren) Fächer (309 − 294)" },
|
||||
"schliessfaecher_unangetastet": { "value": 1291, "description": "Anzahl der nicht aufgebrochenen Schließfächer (1600 − 309)" },
|
||||
"beute_euro": { "value": 10000000, "description": "Geschätzte Beute in Euro (Schmuck, Geld, Gold)" },
|
||||
"material_abtransport_tonnen": { "value": 120, "description": "Abtransportiertes Sand und Geröll aus dem Tunnel in Tonnen (über Monate)" },
|
||||
"parkplaetze_gemietet": { "value": 4, "description": "Anzahl der in der Tiefgarage gemieteten Parkplätze" },
|
||||
"vorbereitung_ab": { "value": "2011-Herbst", "description": "Beginn der Vorbereitung (Herbst 2011, u.a. Schließfach mit gefälschtem Pass gemietet)" },
|
||||
"timeline": [
|
||||
{ "date": "2011-Herbst", "description": "Ein Mann mietet mit gefälschtem niederländischen Pass (Name: Pavel Hatira) ein Schließfach in der Berliner Volksbank Filiale Steglitz." },
|
||||
{ "date": "2012-Anfang", "description": "Tiefgaragenstellplatz in der Wrangelstraße wird gemietet (unter falschem Namen); Rolltor ermöglicht ungestörtes Arbeiten." },
|
||||
{ "date": "2012–2013", "description": "Tunnelbau über viele Monate: ca. 45 m Tunnel, 4 m tief, von Tiefgarage zum Tresorraum; 120 t Sand und Geröll abtransportiert; Durchbruch 30 cm Betonwand (Tiefgarage), 80 cm Stahlbetonwand (Bank)." },
|
||||
{ "date": "2013-01-14", "time": "Nacht", "description": "Einbruch (keine exakten Uhrzeiten öffentlich): Täter dringen über Tunnel in Tresorraum ein, brechen 309 von rund 1600 Schließfächern auf (294 vermietet); Beute ca. 10 Mio €; Feuer zur Spurenvernichtung gelegt." },
|
||||
{ "date": "2013-01-14", "time": "06:15", "description": "Entdeckung: Anwohner bemerkt gegen 6:15 Uhr Rauch in der Tiefgarage; Polizei rückt aus." },
|
||||
{ "date": "2013", "description": "Ermittlungen; Bande wird nie gefasst." }
|
||||
]
|
||||
},
|
||||
"tasks": [
|
||||
{
|
||||
"id": "steglitz-1",
|
||||
"question": "📊 TATORT-STATISTIK (Betroffene Fächer):\n\nIm Tresorraum der Berliner Volksbank Steglitz gab es rund 1600 Schließfächer. Die Täter brachen 309 davon auf; 294 der aufgebrochenen Fächer waren vermietet.\n\nWie viele der aufgebrochenen Fächer waren nicht vermietet (also leer)? – Relevant für Schadensmeldungen und Anzahl der Geschädigten.",
|
||||
"answer": 15,
|
||||
"points": 5,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "steglitz-2",
|
||||
"question": "📐 SCHADENSPRO EINHEIT (Ermittler-Kennzahl):\n\nDie Beute aus dem Tunnel-Bankraub wurde auf etwa 10 Millionen Euro geschätzt. Es wurden 309 Schließfächer aufgebrochen.\n\nWie viel Euro Beute entfielen durchschnittlich pro aufgebrochenes Fach? (Runde auf ganze Euro.)",
|
||||
"answer": 32362,
|
||||
"points": 15,
|
||||
"type": "word-problem"
|
||||
},
|
||||
{
|
||||
"id": "steglitz-3",
|
||||
"question": "📐 TATORT (Betroffene Fächer):\n\nIm Tresorraum der Berliner Volksbank Steglitz gab es rund 1600 Schließfächer. Die Täter brachen 309 davon auf.\n\nWie viele Schließfächer blieben unangetastet?",
|
||||
"answer": 1291,
|
||||
"points": 5,
|
||||
"type": "word-problem"
|
||||
}
|
||||
]
|
||||
}
|
||||
189
public/app.js
189
public/app.js
@@ -1,4 +1,3 @@
|
||||
// ─── Supabase config (injected by server from .env via /config.js) ───────────
|
||||
const supabaseClient = supabase.createClient(window.SUPABASE_URL, window.SUPABASE_ANON_KEY);
|
||||
|
||||
// ─── App state ───────────────────────────────────────────────────────────────
|
||||
@@ -7,6 +6,7 @@ let tasks = [];
|
||||
let chapters = [];
|
||||
let chapterDescriptions = {};
|
||||
let activeChapter = null;
|
||||
let activeClassData = null;
|
||||
|
||||
// ─── Auth ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -22,7 +22,6 @@ async function login() {
|
||||
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;
|
||||
}
|
||||
@@ -43,11 +42,9 @@ async function register() {
|
||||
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() {
|
||||
@@ -78,34 +75,140 @@ async function apiFetch(url, options = {}) {
|
||||
return res;
|
||||
}
|
||||
|
||||
// ─── Theme ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function applyTheme(theme) {
|
||||
const root = document.documentElement;
|
||||
Object.entries(theme).forEach(([prop, value]) => {
|
||||
root.style.setProperty(prop, value);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── 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 = '<p style="color: var(--theme-muted)">Laden...</p>';
|
||||
|
||||
const res = await apiFetch('/api/classes');
|
||||
if (!res) return;
|
||||
const { classes } = await res.json();
|
||||
|
||||
listEl.innerHTML = '';
|
||||
|
||||
if (classes.length === 0) {
|
||||
listEl.innerHTML = '<p style="color: var(--theme-muted)">Keine Klassen verfügbar.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
classes.forEach(cls => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'class-card';
|
||||
card.style.setProperty('--card-accent', cls.theme['--theme-accent'] || 'var(--theme-accent)');
|
||||
card.innerHTML = `
|
||||
<div class="class-card-icon">${cls.icon || '📚'}</div>
|
||||
<div class="class-card-info">
|
||||
<div class="class-card-name">${cls.name}</div>
|
||||
<div class="class-card-desc">${cls.description || ''}</div>
|
||||
</div>
|
||||
`;
|
||||
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;
|
||||
|
||||
document.getElementById('authOverlay').style.display = 'none';
|
||||
document.getElementById('appWrapper').style.display = 'flex';
|
||||
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();
|
||||
}
|
||||
|
||||
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();
|
||||
showScreen('auth');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -151,6 +254,42 @@ function isChapterCompleted(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 = '';
|
||||
@@ -163,7 +302,7 @@ function renderSidebar() {
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'sidebar-header';
|
||||
header.innerHTML = '<h2>Aktenübersicht</h2>';
|
||||
header.innerHTML = '<h2>Lektionen</h2>';
|
||||
sidebar.appendChild(header);
|
||||
|
||||
chapters.forEach(chapter => {
|
||||
@@ -174,6 +313,7 @@ function renderSidebar() {
|
||||
item.innerHTML = `<div class="sidebar-item-title">${isCompleted ? '✓ ' : ''}${chapter}</div>`;
|
||||
sidebar.appendChild(item);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function switchChapter(chapter) {
|
||||
@@ -204,8 +344,8 @@ function renderTasks() {
|
||||
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 class="task-title">Aufgabe</div>
|
||||
<div class="task-points">${task.points} Punkte</div>
|
||||
</div>
|
||||
<div class="task-question">${task.question}</div>
|
||||
<div class="task-input-group">
|
||||
@@ -214,7 +354,7 @@ function renderTasks() {
|
||||
<button class="task-button" onclick="checkAnswer('${task.id}')">Prüfen</button>
|
||||
</div>
|
||||
<div class="task-status ${hasAnswer && !task.isCorrect ? 'error' : ''}" id="status-${task.id}">
|
||||
${hasAnswer && !task.isCorrect ? '✗ Berechnung fehlerhaft. Bitte erneut prüfen.' : ''}
|
||||
${hasAnswer && !task.isCorrect ? '✗ Falsche Antwort. Bitte erneut versuchen.' : ''}
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(card);
|
||||
@@ -248,18 +388,23 @@ async function checkAnswer(taskId) {
|
||||
calculateTotalPoints();
|
||||
|
||||
if (data.correct) {
|
||||
status.textContent = '✓ Berechnung korrekt. Fall weiter bearbeiten.';
|
||||
status.textContent = '✓ Richtig!';
|
||||
status.className = 'task-status success';
|
||||
showMessage(`✓ ${data.points} Ermittlungspunkte erhalten`, '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(); }, 1500);
|
||||
setTimeout(() => {
|
||||
renderSidebar();
|
||||
renderTasks();
|
||||
updateProgress();
|
||||
checkClassCompletion();
|
||||
}, 1500);
|
||||
}
|
||||
} else {
|
||||
status.textContent = '✗ Berechnung fehlerhaft. Bitte erneut prüfen.';
|
||||
status.textContent = '✗ Falsche Antwort. Bitte erneut versuchen.';
|
||||
status.className = 'task-status error';
|
||||
input.focus();
|
||||
renderSidebar();
|
||||
@@ -268,7 +413,7 @@ async function checkAnswer(taskId) {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── UI helpers ───────────────────────────────────────────────────────────────
|
||||
// ─── UI helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
function updatePointsDisplay() {
|
||||
document.getElementById('totalPoints').textContent = totalPoints;
|
||||
|
||||
@@ -75,17 +75,45 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main app — hidden until logged in -->
|
||||
<!-- Class picker — shown after login when no class is active -->
|
||||
<div id="classPickerOverlay" class="class-picker-overlay" style="display: none">
|
||||
<div class="class-picker-card">
|
||||
<h2 class="class-picker-title">Klasse wählen</h2>
|
||||
<p class="class-picker-subtitle">Wähle deine Klasse, um fortzufahren.</p>
|
||||
<div id="classList" class="class-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Class completion — shown when all lessons in the active class are done -->
|
||||
<div id="classCompletionOverlay" class="class-picker-overlay" style="display: none">
|
||||
<div class="class-picker-card" style="text-align: center">
|
||||
<div class="completion-icon">🎉</div>
|
||||
<h2 class="class-picker-title">Klasse abgeschlossen!</h2>
|
||||
<p class="class-picker-subtitle">
|
||||
Du hast alle Aufgaben gelöst und
|
||||
<strong id="completionPoints" style="color: var(--theme-accent)">0</strong>
|
||||
Punkte erreicht.
|
||||
</p>
|
||||
<button class="auth-button" style="margin-top: 24px" onclick="completeAndPickClass()">
|
||||
Nächste Klasse wählen
|
||||
</button>
|
||||
<button class="reset-button" style="margin-top: 10px" onclick="resetClassProgress(this)">
|
||||
Fortschritt zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main app — hidden until logged in and class is active -->
|
||||
<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>
|
||||
<h1>Mathquest</h1>
|
||||
<div class="header-right">
|
||||
<div class="points-display">
|
||||
<div class="points-label">Ermittlungspunkte</div>
|
||||
<div class="points-label">Punkte</div>
|
||||
<div class="points-value" id="totalPoints">0</div>
|
||||
</div>
|
||||
<div class="user-display">
|
||||
@@ -100,7 +128,7 @@
|
||||
|
||||
<div class="progress-section">
|
||||
<div class="progress-content">
|
||||
<div class="progress-label">Ermittlungsfortschritt</div>
|
||||
<div class="progress-label">Fortschritt</div>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" id="progressBar"></div>
|
||||
</div>
|
||||
|
||||
316
public/style.css
316
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;
|
||||
@@ -543,11 +648,11 @@ h1 {
|
||||
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 {
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
174
server.js
174
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 }) => {
|
||||
const cls = JSON.parse(fs.readFileSync(path.join(CLASSES_DIR, file), "utf8"));
|
||||
classes[cls.id] = cls;
|
||||
} catch (err) {
|
||||
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 (caseData.tasks) {
|
||||
caseData.tasks.forEach(
|
||||
({ userAnswer, isCorrect, attempts, answerTimestamp, ...task }) => {
|
||||
});
|
||||
} else if (lessonData.tasks) {
|
||||
lessonData.tasks.forEach(({ userAnswer, isCorrect, attempts, answerTimestamp, ...task }) => {
|
||||
allTasks.push({
|
||||
...task,
|
||||
chapter: caseData.chapter,
|
||||
chapterDescription: caseData.description,
|
||||
chapter: lessonData.chapter,
|
||||
chapterDescription: lessonData.description,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Fehler beim Laden von", file, ":", err.message);
|
||||
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, () => {
|
||||
|
||||
Reference in New Issue
Block a user