From 61e0efaa68e585e668424f68c5cc83efef89a44b Mon Sep 17 00:00:00 2001 From: Lukas Cremer Date: Sun, 25 Jan 2026 19:32:08 +0100 Subject: [PATCH] add chapters and deploy script --- DEPLOY.md | 226 +++++++++++++++++++++++++++++++++++++++++++ deploy.sh | 103 ++++++++++++++++++++ mathquest.service | 18 ++++ nginx-mathquest.conf | 22 +++++ public/app.js | 144 ++++++++++++++++++++++++--- public/index.html | 3 + public/style.css | 83 +++++++++++++++- server.js | 11 ++- 8 files changed, 592 insertions(+), 18 deletions(-) create mode 100644 DEPLOY.md create mode 100755 deploy.sh create mode 100644 mathquest.service create mode 100644 nginx-mathquest.conf diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..eff5089 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,226 @@ +# MathQuest Deployment Anleitung + +Diese Anleitung erklärt, wie du MathQuest auf deinem Server deployst. + +## Voraussetzungen + +- Server mit Ubuntu/Debian +- Node.js installiert (Version 14 oder höher) +- Nginx installiert +- SSL-Zertifikat für lydix.de (Let's Encrypt) +- Root-Zugriff oder sudo-Rechte + +## Schritt 1: Dateien auf den Server kopieren + +### Option A: Mit SCP (vom lokalen Rechner) + +```bash +# Vom lokalen Rechner aus: +scp -r /home/lukas/nerd/mathquest/* user@dein-server:/var/www/mathquest/ +``` + +### Option B: Mit Git (wenn du ein Repository hast) + +```bash +# Auf dem Server: +cd /var/www +git clone dein-repo-url mathquest +cd mathquest +``` + +## Schritt 2: Dependencies installieren + +```bash +cd /var/www/mathquest +npm install --production +``` + +## Schritt 3: Berechtigungen setzen + +```bash +chown -R www-data:www-data /var/www/mathquest +chmod +x /var/www/mathquest/server.js +``` + +## Schritt 4: systemd Service installieren + +```bash +# Service-Datei kopieren +sudo cp /var/www/mathquest/mathquest.service /etc/systemd/system/ + +# Service aktivieren und starten +sudo systemctl daemon-reload +sudo systemctl enable mathquest +sudo systemctl start mathquest + +# Status prüfen +sudo systemctl status mathquest +``` + +## Schritt 5: Nginx konfigurieren + +### SSL-Zertifikat prüfen + +Da du einen Wildcard DNS Eintrag hast, sollte ein Wildcard-Zertifikat für `*.lydix.de` existieren. + +Prüfe deine Zertifikate: +```bash +sudo certbot certificates +``` + +Finde den korrekten Pfad (oft `/etc/letsencrypt/live/lydix.de/` oder `/etc/letsencrypt/live/lydix.de-0001/`): +```bash +ls -la /etc/letsencrypt/live/ +``` + +Falls der Pfad anders ist, passe die SSL-Zertifikat-Pfade in `nginx-mathquest.conf` an. + +### Nginx-Konfiguration installieren + +```bash +# Konfiguration kopieren +sudo cp /var/www/mathquest/nginx-mathquest.conf /etc/nginx/sites-available/mathquest.conf + +# Symlink erstellen +sudo ln -s /etc/nginx/sites-available/mathquest.conf /etc/nginx/sites-enabled/ + +# Konfiguration testen +sudo nginx -t + +# Nginx neu laden +sudo systemctl reload nginx +``` + +## Schritt 6: DNS konfigurieren + +Da du einen Wildcard DNS Eintrag hast (`*.lydix.de`), ist kein separater A-Record für `mathquest.lydix.de` nötig. Die Subdomain sollte automatisch funktionieren. + +Falls der Wildcard-Eintrag noch nicht existiert, erstelle einen: +``` +*.lydix.de A deine-server-ip +``` + +## Schritt 7: Firewall prüfen + +Stelle sicher, dass Port 80 und 443 offen sind: + +```bash +# UFW +sudo ufw allow 80/tcp +sudo ufw allow 443/tcp + +# Oder iptables +sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT +sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT +``` + +## Automatisches Deployment + +Du kannst auch das bereitgestellte Deployment-Script verwenden: + +```bash +cd /var/www/mathquest +chmod +x deploy.sh +sudo ./deploy.sh +``` + +## Nützliche Befehle + +### Service verwalten + +```bash +# Status prüfen +sudo systemctl status mathquest + +# Logs ansehen +sudo journalctl -u mathquest -f + +# Service neustarten +sudo systemctl restart mathquest + +# Service stoppen +sudo systemctl stop mathquest + +# Service starten +sudo systemctl start mathquest +``` + +### Nginx verwalten + +```bash +# Konfiguration testen +sudo nginx -t + +# Nginx neu laden +sudo systemctl reload nginx + +# Nginx neustarten +sudo systemctl restart nginx + +# Logs ansehen +sudo tail -f /var/log/nginx/mathquest-access.log +sudo tail -f /var/log/nginx/mathquest-error.log +``` + +## Troubleshooting + +### Service startet nicht + +```bash +# Prüfe die Logs +sudo journalctl -u mathquest -n 50 + +# Prüfe ob Port 3000 belegt ist +sudo netstat -tulpn | grep 3000 + +# Prüfe die Berechtigungen +ls -la /var/www/mathquest +``` + +### Nginx gibt 502 Bad Gateway + +```bash +# Prüfe ob der Service läuft +sudo systemctl status mathquest + +# Prüfe die Nginx-Logs +sudo tail -f /var/log/nginx/mathquest-error.log + +# Prüfe ob Node.js auf Port 3000 lauscht +sudo netstat -tulpn | grep 3000 +``` + +### SSL-Zertifikat Probleme + +```bash +# Prüfe Zertifikat +sudo certbot certificates + +# Erneuere Zertifikat falls nötig +sudo certbot renew + +# Prüfe Zertifikat-Pfade in Nginx-Konfiguration +sudo cat /etc/nginx/sites-available/mathquest.conf | grep ssl_certificate +``` + +## Updates durchführen + +1. Dateien auf dem Server aktualisieren +2. Dependencies aktualisieren (falls nötig): + ```bash + cd /var/www/mathquest + npm install --production + ``` +3. Service neustarten: + ```bash + sudo systemctl restart mathquest + ``` + +## Backup + +Wichtig: Backupe regelmäßig die `tasks.json` Datei, da dort alle Antworten gespeichert werden: + +```bash +# Backup erstellen +cp /var/www/mathquest/tasks.json /var/www/mathquest/tasks.json.backup +``` diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..7cba1fc --- /dev/null +++ b/deploy.sh @@ -0,0 +1,103 @@ +#!/bin/bash + +# MathQuest Deployment Script +# Führe dieses Script auf dem Server aus + +set -e + +echo "🚀 MathQuest Deployment Script" +echo "================================" +echo "" + +# Farben für Output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Variablen +APP_DIR="/var/www/mathquest" +SERVICE_NAME="mathquest" +NGINX_CONF="/etc/nginx/sites-available/mathquest.conf" +NGINX_ENABLED="/etc/nginx/sites-enabled/mathquest.conf" + +# Prüfe ob als root ausgeführt +if [ "$EUID" -ne 0 ]; then + echo -e "${RED}❌ Bitte führe dieses Script als root aus (sudo ./deploy.sh)${NC}" + exit 1 +fi + +echo -e "${YELLOW}📦 Schritt 1: Erstelle Verzeichnis...${NC}" +mkdir -p $APP_DIR +echo -e "${GREEN}✅ Verzeichnis erstellt${NC}" + +echo "" +echo -e "${YELLOW}📝 Schritt 2: Kopiere Dateien...${NC}" +echo "Bitte kopiere die Dateien manuell nach $APP_DIR" +echo "Oder verwende: scp -r * user@server:$APP_DIR/" +echo "" +read -p "Drücke Enter, wenn die Dateien kopiert wurden..." + +echo "" +echo -e "${YELLOW}📦 Schritt 3: Installiere Dependencies...${NC}" +cd $APP_DIR +if [ -f "package.json" ]; then + npm install --production + echo -e "${GREEN}✅ Dependencies installiert${NC}" +else + echo -e "${RED}❌ package.json nicht gefunden!${NC}" + exit 1 +fi + +echo "" +echo -e "${YELLOW}🔧 Schritt 4: Setze Berechtigungen...${NC}" +chown -R www-data:www-data $APP_DIR +chmod +x $APP_DIR/server.js +echo -e "${GREEN}✅ Berechtigungen gesetzt${NC}" + +echo "" +echo -e "${YELLOW}⚙️ Schritt 5: Installiere systemd Service...${NC}" +if [ -f "$APP_DIR/mathquest.service" ]; then + cp $APP_DIR/mathquest.service /etc/systemd/system/$SERVICE_NAME.service + systemctl daemon-reload + systemctl enable $SERVICE_NAME + echo -e "${GREEN}✅ Service installiert und aktiviert${NC}" +else + echo -e "${RED}❌ mathquest.service nicht gefunden!${NC}" + exit 1 +fi + +echo "" +echo -e "${YELLOW}🌐 Schritt 6: Konfiguriere Nginx...${NC}" +if [ -f "$APP_DIR/nginx-mathquest.conf" ]; then + cp $APP_DIR/nginx-mathquest.conf $NGINX_CONF + ln -sf $NGINX_CONF $NGINX_ENABLED + nginx -t + if [ $? -eq 0 ]; then + systemctl reload nginx + echo -e "${GREEN}✅ Nginx konfiguriert${NC}" + else + echo -e "${RED}❌ Nginx Konfiguration fehlerhaft!${NC}" + exit 1 + fi +else + echo -e "${RED}❌ nginx-mathquest.conf nicht gefunden!${NC}" + exit 1 +fi + +echo "" +echo -e "${YELLOW}🚀 Schritt 7: Starte Service...${NC}" +systemctl start $SERVICE_NAME +sleep 2 +systemctl status $SERVICE_NAME --no-pager + +echo "" +echo -e "${GREEN}✅ Deployment abgeschlossen!${NC}" +echo "" +echo "Nützliche Befehle:" +echo " Status prüfen: systemctl status $SERVICE_NAME" +echo " Logs ansehen: journalctl -u $SERVICE_NAME -f" +echo " Neustarten: systemctl restart $SERVICE_NAME" +echo " Stoppen: systemctl stop $SERVICE_NAME" +echo "" +echo "Die App sollte jetzt unter https://mathquest.lydix.de erreichbar sein!" diff --git a/mathquest.service b/mathquest.service new file mode 100644 index 0000000..8b7cc8d --- /dev/null +++ b/mathquest.service @@ -0,0 +1,18 @@ +[Unit] +Description=MathQuest - Mathe-Lernseite für Kinder +After=network.target + +[Service] +Type=simple +User=www-data +WorkingDirectory=/var/www/mathquest +ExecStart=/usr/bin/node /var/www/mathquest/server.js +Restart=always +RestartSec=10 +StandardOutput=journal +StandardError=journal +Environment=NODE_ENV=production +Environment=PORT=3000 + +[Install] +WantedBy=multi-user.target diff --git a/nginx-mathquest.conf b/nginx-mathquest.conf new file mode 100644 index 0000000..3bd5ce0 --- /dev/null +++ b/nginx-mathquest.conf @@ -0,0 +1,22 @@ +server { + listen 80; + server_name mathquest.lydix.de; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + server_name mathquest.lydix.de; + ssl_certificate /etc/letsencrypt/live/mathquest.lydix.de/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/mathquest.lydix.de/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + location / { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + } +} diff --git a/public/app.js b/public/app.js index f387c43..683a453 100644 --- a/public/app.js +++ b/public/app.js @@ -4,11 +4,15 @@ const API_BASE = ''; // Globale Variablen let totalPoints = 0; let tasks = []; +let chapters = []; +let activeChapter = null; // Initialisiere die App async function init() { await loadTasks(); + extractChapters(); calculateTotalPoints(); + renderTabs(); renderTasks(); updateProgress(); } @@ -33,17 +37,79 @@ async function loadTasks() { } } +// Extrahiere alle Chapters aus den Tasks +function extractChapters() { + const chapterSet = new Set(); + tasks.forEach(task => { + if (task.chapter) { + chapterSet.add(task.chapter); + } + }); + chapters = Array.from(chapterSet).sort(); + + // Setze ersten Chapter als aktiv, falls vorhanden + if (chapters.length > 0 && !activeChapter) { + activeChapter = chapters[0]; + } else if (chapters.length === 0) { + // Wenn keine Chapters vorhanden, zeige alle Tasks + activeChapter = null; + } +} + +// Rendere Tabs für alle Chapters +function renderTabs() { + const tabsContainer = document.getElementById('tabsContainer'); + tabsContainer.innerHTML = ''; + + // Zeige Tabs nur an, wenn es Chapters gibt + if (chapters.length === 0) { + tabsContainer.style.display = 'none'; + return; + } + + tabsContainer.style.display = 'flex'; + + chapters.forEach(chapter => { + const tabButton = document.createElement('button'); + tabButton.className = `tab-button ${activeChapter === chapter ? 'active' : ''}`; + tabButton.textContent = chapter; + tabButton.onclick = () => switchChapter(chapter); + tabsContainer.appendChild(tabButton); + }); +} + +// Wechsle zu einem anderen Chapter +function switchChapter(chapter) { + activeChapter = chapter; + renderTabs(); + renderTasks(); +} + +// Hole Tasks für das aktive Chapter +function getTasksForActiveChapter() { + if (!activeChapter) { + return tasks; + } + return tasks.filter(task => task.chapter === activeChapter); +} + // Rendere Aufgaben function renderTasks() { const container = document.getElementById('taskContainer'); container.innerHTML = ''; - tasks.forEach(task => { + const tasksToShow = getTasksForActiveChapter(); + + tasksToShow.forEach(task => { + // Überspringe bereits richtig beantwortete Aufgaben (werden nicht mehr angezeigt) + if (task.isCorrect === true) { + return; + } + const taskCard = document.createElement('div'); taskCard.className = 'task-card'; taskCard.id = `task-${task.id}`; - const isCompleted = task.isCorrect === true; const hasAnswer = task.userAnswer !== undefined; taskCard.innerHTML = ` @@ -59,25 +125,20 @@ function renderTasks() { id="input-${task.id}" placeholder="?" value="${hasAnswer ? task.userAnswer : ''}" - ${isCompleted ? 'disabled' : ''} >
- ${isCompleted ? '✅ Richtig beantwortet!' : hasAnswer && !isCompleted ? '❌ Falsch - versuche es nochmal!' : ''} + ${hasAnswer && !task.isCorrect ? '❌ Falsch - versuche es nochmal!' : ''}
`; - if (isCompleted) { - const statusEl = taskCard.querySelector('.task-status'); - statusEl.className = 'task-status success'; - } else if (hasAnswer && !isCompleted) { + if (hasAnswer && !task.isCorrect) { const statusEl = taskCard.querySelector('.task-status'); statusEl.className = 'task-status error'; } @@ -124,27 +185,41 @@ async function checkAnswer(taskId) { // Lade Tasks neu, um die gespeicherte Antwort zu erhalten await loadTasks(); + extractChapters(); calculateTotalPoints(); if (data.correct) { status.textContent = '🎉 Richtig! Super gemacht!'; status.className = 'task-status success'; - // Deaktiviere Input und Button - input.disabled = true; - button.disabled = true; - // Zeige Erfolgsnachricht showMessage(`🎉 ${data.points} Quest-Punkte verdient!`, 'success'); + + // Konfetti-Animation und Aufgabe verschwinden lassen + const taskCard = document.getElementById(`task-${taskId}`); + if (taskCard) { + createConfetti(taskCard); + taskCard.classList.add('confetti-animation'); + + // Nach Animation die Aufgabe entfernen + setTimeout(() => { + renderTabs(); + renderTasks(); + updateProgress(); + }, 1500); + } } else { status.textContent = '❌ Nicht ganz richtig. Versuch es nochmal!'; status.className = 'task-status error'; input.focus(); } - // Rendere Tasks neu, um den aktuellen Status anzuzeigen - renderTasks(); - updateProgress(); + // Rendere Tabs und Tasks neu, um den aktuellen Status anzuzeigen (nur wenn nicht korrekt) + if (!data.correct) { + renderTabs(); + renderTasks(); + updateProgress(); + } } catch (error) { console.error('Fehler beim Prüfen der Antwort:', error); status.textContent = 'Fehler beim Prüfen. Bitte versuche es erneut.'; @@ -174,6 +249,43 @@ function updateProgress() { progressText.textContent = `${totalPoints} / ${maxPoints}`; } +// Erstelle Konfetti-Animation +function createConfetti(element) { + const colors = ['#ffd700', '#ff6b6b', '#4ecdc4', '#45b7d1', '#f9ca24', '#f0932b', '#eb4d4b', '#6c5ce7']; + const confettiCount = 50; + + // Hole Position des Elements + const rect = element.getBoundingClientRect(); + const container = element.closest('.container') || document.body; + + for (let i = 0; i < confettiCount; i++) { + const confetti = document.createElement('div'); + confetti.className = 'confetti'; + + // Zufällige Position relativ zum Element + const randomX = (Math.random() - 0.5) * 2; // -1 bis 1 + 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); + + // Entferne Konfetti nach Animation + setTimeout(() => { + if (confetti.parentNode) { + confetti.remove(); + } + }, 2000); + } +} + // Zeige Nachricht function showMessage(text, type) { const message = document.getElementById('message'); diff --git a/public/index.html b/public/index.html index c39f9b2..932f422 100644 --- a/public/index.html +++ b/public/index.html @@ -26,6 +26,9 @@

Mathe-Aufgaben

+
+ +
diff --git a/public/style.css b/public/style.css index de7ade1..392cc53 100644 --- a/public/style.css +++ b/public/style.css @@ -100,6 +100,45 @@ h1 { font-size: 1.8em; } +.tabs-container { + display: flex; + gap: 10px; + margin-bottom: 20px; + flex-wrap: wrap; + justify-content: center; +} + +.tab-button { + background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); + color: #333; + border: none; + padding: 12px 20px; + font-size: 1.1em; + border-radius: 10px; + cursor: pointer; + font-family: 'Comic Sans MS', cursive; + font-weight: bold; + transition: all 0.3s ease; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + position: relative; +} + +.tab-button:hover { + transform: translateY(-2px); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); +} + +.tab-button.active { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); +} + +.tab-button.active:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5); +} + .task-container { display: grid; gap: 20px; @@ -110,7 +149,9 @@ h1 { padding: 25px; border-radius: 15px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); - transition: transform 0.2s, box-shadow 0.2s; + transition: transform 0.2s, box-shadow 0.2s, opacity 0.5s ease, transform 0.5s ease; + position: relative; + overflow: hidden; } .task-card:hover { @@ -118,6 +159,46 @@ h1 { box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15); } +.task-card.confetti-animation { + animation: confettiFadeOut 1.5s ease forwards; +} + +@keyframes confettiFadeOut { + 0% { + transform: scale(1) rotate(0deg); + opacity: 1; + } + 50% { + transform: scale(1.1) rotate(5deg); + opacity: 0.8; + } + 100% { + transform: scale(0) rotate(10deg); + opacity: 0; + } +} + +.confetti { + position: absolute; + width: 10px; + height: 10px; + background: #ffd700; + animation: confettiFall linear forwards; + z-index: 1000; + pointer-events: none; +} + +@keyframes confettiFall { + 0% { + transform: translateY(0) translateX(0) rotate(0deg); + opacity: 1; + } + 100% { + transform: translateY(400px) translateX(calc(var(--random-x, 0) * 100px)) rotate(720deg); + opacity: 0; + } +} + .task-header { display: flex; justify-content: space-between; diff --git a/server.js b/server.js index 06e4437..f3dae64 100644 --- a/server.js +++ b/server.js @@ -55,9 +55,18 @@ app.post('/api/check-answer', (req, res) => { const correctAnswer = parseInt(task.answer); const isCorrect = userAnswer === correctAnswer; - // Speichere Antwort und isCorrect in der Task + // Initialisiere attempts Counter falls nicht vorhanden + if (tasks[taskIndex].attempts === undefined) { + tasks[taskIndex].attempts = 0; + } + + // Erhöhe Versuchszähler bei jeder Antwortprüfung + tasks[taskIndex].attempts += 1; + + // Speichere Antwort, isCorrect und Timestamp in der Task tasks[taskIndex].userAnswer = userAnswer; tasks[taskIndex].isCorrect = isCorrect; + tasks[taskIndex].answerTimestamp = new Date().toISOString(); saveTasks(tasks); res.json({