add chapters and deploy script

This commit is contained in:
Lukas Cremer
2026-01-25 19:32:08 +01:00
parent 37b77fdb40
commit 61e0efaa68
8 changed files with 592 additions and 18 deletions

226
DEPLOY.md Normal file
View File

@@ -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
```

103
deploy.sh Executable file
View File

@@ -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!"

18
mathquest.service Normal file
View File

@@ -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

22
nginx-mathquest.conf Normal file
View File

@@ -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;
}
}

View File

@@ -4,11 +4,15 @@ const API_BASE = '';
// Globale Variablen // Globale Variablen
let totalPoints = 0; let totalPoints = 0;
let tasks = []; let tasks = [];
let chapters = [];
let activeChapter = null;
// Initialisiere die App // Initialisiere die App
async function init() { async function init() {
await loadTasks(); await loadTasks();
extractChapters();
calculateTotalPoints(); calculateTotalPoints();
renderTabs();
renderTasks(); renderTasks();
updateProgress(); 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 // Rendere Aufgaben
function renderTasks() { function renderTasks() {
const container = document.getElementById('taskContainer'); const container = document.getElementById('taskContainer');
container.innerHTML = ''; 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'); const taskCard = document.createElement('div');
taskCard.className = 'task-card'; taskCard.className = 'task-card';
taskCard.id = `task-${task.id}`; taskCard.id = `task-${task.id}`;
const isCompleted = task.isCorrect === true;
const hasAnswer = task.userAnswer !== undefined; const hasAnswer = task.userAnswer !== undefined;
taskCard.innerHTML = ` taskCard.innerHTML = `
@@ -59,25 +125,20 @@ function renderTasks() {
id="input-${task.id}" id="input-${task.id}"
placeholder="?" placeholder="?"
value="${hasAnswer ? task.userAnswer : ''}" value="${hasAnswer ? task.userAnswer : ''}"
${isCompleted ? 'disabled' : ''}
> >
<button <button
class="task-button" class="task-button"
onclick="checkAnswer('${task.id}')" onclick="checkAnswer('${task.id}')"
${isCompleted ? 'disabled' : ''}
> >
Prüfen Prüfen
</button> </button>
</div> </div>
<div class="task-status" id="status-${task.id}"> <div class="task-status" id="status-${task.id}">
${isCompleted ? '✅ Richtig beantwortet!' : hasAnswer && !isCompleted ? '❌ Falsch - versuche es nochmal!' : ''} ${hasAnswer && !task.isCorrect ? '❌ Falsch - versuche es nochmal!' : ''}
</div> </div>
`; `;
if (isCompleted) { if (hasAnswer && !task.isCorrect) {
const statusEl = taskCard.querySelector('.task-status');
statusEl.className = 'task-status success';
} else if (hasAnswer && !isCompleted) {
const statusEl = taskCard.querySelector('.task-status'); const statusEl = taskCard.querySelector('.task-status');
statusEl.className = 'task-status error'; statusEl.className = 'task-status error';
} }
@@ -124,27 +185,41 @@ async function checkAnswer(taskId) {
// Lade Tasks neu, um die gespeicherte Antwort zu erhalten // Lade Tasks neu, um die gespeicherte Antwort zu erhalten
await loadTasks(); await loadTasks();
extractChapters();
calculateTotalPoints(); calculateTotalPoints();
if (data.correct) { if (data.correct) {
status.textContent = '🎉 Richtig! Super gemacht!'; status.textContent = '🎉 Richtig! Super gemacht!';
status.className = 'task-status success'; status.className = 'task-status success';
// Deaktiviere Input und Button
input.disabled = true;
button.disabled = true;
// Zeige Erfolgsnachricht // Zeige Erfolgsnachricht
showMessage(`🎉 ${data.points} Quest-Punkte verdient!`, 'success'); 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 { } else {
status.textContent = '❌ Nicht ganz richtig. Versuch es nochmal!'; status.textContent = '❌ Nicht ganz richtig. Versuch es nochmal!';
status.className = 'task-status error'; status.className = 'task-status error';
input.focus(); input.focus();
} }
// Rendere Tasks neu, um den aktuellen Status anzuzeigen // Rendere Tabs und Tasks neu, um den aktuellen Status anzuzeigen (nur wenn nicht korrekt)
renderTasks(); if (!data.correct) {
updateProgress(); renderTabs();
renderTasks();
updateProgress();
}
} catch (error) { } catch (error) {
console.error('Fehler beim Prüfen der Antwort:', error); console.error('Fehler beim Prüfen der Antwort:', error);
status.textContent = 'Fehler beim Prüfen. Bitte versuche es erneut.'; status.textContent = 'Fehler beim Prüfen. Bitte versuche es erneut.';
@@ -174,6 +249,43 @@ function updateProgress() {
progressText.textContent = `${totalPoints} / ${maxPoints}`; 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 // Zeige Nachricht
function showMessage(text, type) { function showMessage(text, type) {
const message = document.getElementById('message'); const message = document.getElementById('message');

View File

@@ -26,6 +26,9 @@
<div class="task-section"> <div class="task-section">
<h2>Mathe-Aufgaben</h2> <h2>Mathe-Aufgaben</h2>
<div class="tabs-container" id="tabsContainer">
<!-- Tabs werden hier dynamisch eingefügt -->
</div>
<div id="taskContainer" class="task-container"> <div id="taskContainer" class="task-container">
<!-- Aufgaben werden hier dynamisch eingefügt --> <!-- Aufgaben werden hier dynamisch eingefügt -->
</div> </div>

View File

@@ -100,6 +100,45 @@ h1 {
font-size: 1.8em; 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 { .task-container {
display: grid; display: grid;
gap: 20px; gap: 20px;
@@ -110,7 +149,9 @@ h1 {
padding: 25px; padding: 25px;
border-radius: 15px; border-radius: 15px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); 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 { .task-card:hover {
@@ -118,6 +159,46 @@ h1 {
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15); 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 { .task-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

@@ -55,9 +55,18 @@ app.post('/api/check-answer', (req, res) => {
const correctAnswer = parseInt(task.answer); const correctAnswer = parseInt(task.answer);
const isCorrect = userAnswer === correctAnswer; 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].userAnswer = userAnswer;
tasks[taskIndex].isCorrect = isCorrect; tasks[taskIndex].isCorrect = isCorrect;
tasks[taskIndex].answerTimestamp = new Date().toISOString();
saveTasks(tasks); saveTasks(tasks);
res.json({ res.json({