Files
geograsim/App/schueler.html
T
Adminator b356c79134 Atlas: Phase A Integrations-Konzept + Cockpit + End-Screen
- Integrations-Konzept dokumentiert (App/docs/integration-konzept.md)
- Module-IDs vereinheitlicht: sim-XX -> semantische IDs (klima/heli/fluss/logistik/...)
- class_modules um current_level + quiz_enabled Spalten erweitert
- modules.php liefert dynamisch aus module_info, inkl. forcedLevel, quizEnabled,
  play_url, dueDate; neuer Filter ?module_id=X fuer einzeln
- schueler.html Cockpit: komplett API-first, teacher_started-Sektion als
  Auftrags-Block, neue IDs, Sprachregel 4a (Starten statt Spielen)
- teacher.html: Hardcoded ALL_MODS entfernt, laedt dynamisch
- Design-System: .ggs-endscreen-Komponente fuer einheitliche Modul-Abschluesse
- index.html Landing-Links gefixt: App/modul-X -> modul-X, V1-HTMLs auf
  Front-Controller-Routen (fluss, heli-game, stadt)
- 6 Inbox-Auftraege an Instanzen (Klima/Heli/Fluss/Logistik/Lehrplan/Glossar)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 11:42:19 +02:00

302 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoGraSim — Meine Simulationen</title>
<link rel="icon" type="image/png" href="favicon-96x96.png" sizes="96x96"><link rel="icon" type="image/svg+xml" href="favicon.svg">
<link rel="stylesheet" href="assets/fonts/inter.css">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Inter',sans-serif;background:#f7f6f3;color:#1a1a1a;min-height:100vh}
.topbar{background:#fff;padding:.55rem 1.2rem;display:flex;align-items:center;gap:.7rem;border-bottom:1px solid rgba(0,0,0,.06);position:sticky;top:0;z-index:10}
.topbar .logo{height:28px}
.topbar .spacer{flex:1}
.topbar .user-pill{display:flex;align-items:center;gap:.35rem;padding:.25rem .6rem .25rem .3rem;background:#dae8ec;border-radius:99px;font-size:.72rem;font-weight:600;color:#4a7c8a;text-decoration:none;cursor:pointer}
.topbar .user-pill:hover{background:#c8dce2}
.topbar .user-pill .emoji{font-size:1.2rem}
.topbar .btn-sm{padding:.25rem .55rem;border:1px solid rgba(0,0,0,.08);border-radius:6px;background:#fff;font-size:.65rem;font-weight:600;cursor:pointer;color:#4a4a4a;text-decoration:none}
.topbar .btn-sm:hover{background:#dae8ec;color:#4a7c8a}
.wrap{max-width:900px;margin:0 auto;padding:1.2rem 1rem}
/* Tabs */
.tabs{display:flex;gap:0;margin-bottom:1.2rem;border-radius:10px;overflow:hidden;border:1.5px solid rgba(0,0,0,.06);background:#fff}
.tab{flex:1;padding:.6rem;text-align:center;font-size:.78rem;font-weight:700;cursor:pointer;color:#8a8a8a;transition:all .15s;border:none;background:transparent}
.tab.on{background:#4a7c8a;color:#fff}
/* Module grid */
.mod-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:.8rem}
.mod-card{background:#fff;border-radius:12px;overflow:hidden;border:1.5px solid rgba(0,0,0,.05);transition:all .25s;cursor:pointer;text-decoration:none;color:inherit;display:block}
.mod-card:hover{transform:translateY(-3px);box-shadow:0 8px 25px rgba(74,124,138,.1);border-color:rgba(74,124,138,.2)}
.mod-card.locked{opacity:.55;cursor:default}
.mod-card.locked:hover{transform:none;box-shadow:none}
.mod-img{height:110px;background:linear-gradient(135deg,#dae8ec,#c8dce2);display:flex;align-items:center;justify-content:center;font-size:2.5rem;position:relative}
.mod-img img{width:100%;height:100%;object-fit:cover}
.mod-body{padding:.7rem}
.mod-body h3{font-size:.82rem;font-weight:700;margin-bottom:.15rem}
.mod-body p{font-size:.68rem;color:#4a4a4a;margin-bottom:.4rem}
.mod-status{display:inline-flex;align-items:center;gap:.25rem;font-size:.6rem;font-weight:600;padding:2px 8px;border-radius:5px}
.mod-status.available{background:#dceadd;color:#5a8a5e}
.mod-status.locked-s{background:#f0eeea;color:#8a8a8a}
.mod-status.done{background:#dae8ec;color:#4a7c8a}
.lock-icon{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:1.5rem;background:rgba(255,255,255,.9);border-radius:8px;padding:.3rem .6rem}
/* Erfolge */
.badge-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:.6rem}
.badge{background:#fff;border-radius:10px;padding:.8rem;text-align:center;border:1.5px solid rgba(0,0,0,.04)}
.badge .icon{font-size:2rem;margin-bottom:.3rem}
.badge .name{font-size:.72rem;font-weight:700}
.badge .desc{font-size:.6rem;color:#8a8a8a}
.badge.earned{border-color:#4a7c8a;background:linear-gradient(135deg,#f0f8fa,#fff)}
.badge.locked-b{opacity:.4}
/* Profil-Karte */
.profile-card{background:#fff;border-radius:12px;padding:1.5rem;text-align:center;border:1.5px solid rgba(0,0,0,.04)}
.profile-avatar{font-size:3.5rem;cursor:pointer;transition:transform .15s}
.profile-avatar:hover{transform:scale(1.15)}
.profile-name{font-size:1.1rem;font-weight:800;margin-top:.3rem}
.profile-class{font-size:.75rem;color:#8a8a8a}
.profile-stats{display:flex;justify-content:center;gap:1.5rem;margin-top:.8rem}
.profile-stat .n{font-size:1.3rem;font-weight:900;color:#4a7c8a}
.profile-stat .l{font-size:.6rem;color:#8a8a8a}
.hidden{display:none}
@media(max-width:550px){.mod-grid{grid-template-columns:1fr}.badge-grid{grid-template-columns:repeat(2,1fr)}}
</style>
</head>
<body>
<div class="topbar">
<img src="assets/img/geograsim_t.svg" alt="GeoGraSim" class="logo">
<span class="spacer"></span>
<a href="profil.html" class="user-pill" id="user-pill"><span class="emoji">🧑‍🎓</span><span id="user-name"></span></a>
<button class="btn-sm" onclick="logout()">Abmelden</button>
</div>
<div class="wrap">
<!-- Tabs -->
<div class="tabs">
<div class="tab on" onclick="switchTab('modules')">Meine Module</div>
<div class="tab" onclick="switchTab('achievements')">Meine Erfolge</div>
<div class="tab" onclick="switchTab('profile')">Profil</div>
</div>
<div id="tab-modules"></div>
<div id="tab-achievements" class="hidden"></div>
<div id="tab-profile" class="hidden"></div>
</div>
<script>
var BASE = (window.__GGS__?.baseUrl) || '';
if (!BASE) { var p = location.pathname, i = p.indexOf('/geograsim'); if (i !== -1) { var rest = p.substring(i + '/geograsim'.length); BASE = rest.indexOf('/App/') === 0 ? p.substring(0, i) + '/geograsim/App' : p.substring(0, i) + '/geograsim'; } }
var profile = null;
var modules = [];
// Module kommen aus /api/modules?student=1 (inkl. play_url, icon, name, mode).
// card_image + Fallback-Beschreibung ergaenzen wir clientseitig.
var CARD_IMAGES = {
'klima': 'assets/img/card-climate.png',
'klima-3d': 'assets/img/card-climate.png',
'fluss': 'assets/img/card-fluss.png',
'heli': 'assets/img/card-heli.png',
'stadt': 'assets/img/card-city.png',
'erdbeben': 'assets/img/card-tectonic.png',
'energiemix': 'assets/img/card-energy.png',
'lieferketten': 'assets/img/card-globe.png',
'regenwald': 'assets/img/card-climate.png',
'logistik': 'assets/img/card-globe.png'
};
var BADGES = [
{id:'first_sim', icon:'🎯', name:'Erste Simulation', desc:'Eine Simulation abgeschlossen'},
{id:'three_sims', icon:'🏅', name:'Dreifach', desc:'Drei verschiedene Simulationen gespielt'},
{id:'all_sims', icon:'🏆', name:'Komplett', desc:'Alle Module abgeschlossen'},
{id:'high_score', icon:'⭐', name:'Spitzenscore', desc:'Score über 80 in einer Simulation'},
{id:'speed_run', icon:'⚡', name:'Blitzschnell', desc:'Eine Simulation in unter 10 Minuten'},
{id:'explorer', icon:'🧭', name:'Entdecker', desc:'Alle Maßnahmen in einer Simulation ausprobiert'},
{id:'climate_hero', icon:'🌍', name:'Klimaheld', desc:'Temperaturanstieg unter 1.5°C gehalten'},
{id:'city_planner', icon:'🏙️', name:'Stadtplaner', desc:'Stadt mit über 90% Zufriedenheit gebaut'},
];
async function api(endpoint, data) {
var opts = data ? {method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',body:JSON.stringify(data)} : {credentials:'same-origin'};
var r = await fetch(BASE + '/api/' + endpoint, opts);
return await r.json();
}
function logout() { api('auth', {action:'logout'}).then(function() { location.href = 'login.html'; }); }
function switchTab(tab) {
document.querySelectorAll('.tab').forEach(function(t, i) {
var tabs = ['modules','achievements','profile'];
t.classList.toggle('on', tabs[i] === tab);
});
document.getElementById('tab-modules').classList.toggle('hidden', tab !== 'modules');
document.getElementById('tab-achievements').classList.toggle('hidden', tab !== 'achievements');
document.getElementById('tab-profile').classList.toggle('hidden', tab !== 'profile');
}
async function init() {
var status = await api('auth', {action:'status'});
if (status.role !== 'student') { location.href = 'login.html'; return; }
document.getElementById('user-name').textContent = status.session?.display_name || 'Schüler';
// Load profile
var pr = await api('profile');
profile = pr.profile;
if (profile?.emoji_avatar) {
document.querySelector('#user-pill .emoji').textContent = profile.emoji_avatar;
}
await renderModules();
renderAchievements();
renderProfile();
}
function buildModuleUrl(mod) {
// Bevorzugt play_url aus module_info; Fallback: module_id
var base = mod.play_url || mod.id;
// Relativ zum aktuellen Front-Controller-Pfad — Apache-Rewrite macht den Rest
var url = base.indexOf('/') === 0 ? base : base;
if (mod.mode === 'teacher_started' && mod.forcedLevel) {
url += (url.indexOf('?') === -1 ? '?' : '&') + 'level=' + mod.forcedLevel;
}
return url;
}
async function renderModules() {
var html = '';
var modList = [];
try {
modList = await api('modules?student=1');
if (modList.error) modList = [];
} catch(e) {}
// Lehrer-gesteuerte Auftraege zuerst
var teacherStarted = modList.filter(function(m) { return m.mode === 'teacher_started'; });
if (teacherStarted.length > 0) {
html += '<div style="margin-bottom:1rem">';
html += '<p style="font-size:.78rem;font-weight:700;color:#e8833a;margin-bottom:.4rem">📋 Auftraege von deiner Lehrperson</p>';
teacherStarted.forEach(function(m) {
var due = m.dueDate ? '<br><span style="font-size:.6rem;color:#8a8a8a">Zu erledigen bis ' + new Date(m.dueDate).toLocaleDateString('de-AT',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}) + '</span>' : '';
var quiz = m.quizEnabled ? '<br><span style="font-size:.6rem;color:#4a7c8a">📝 Mit kurzem Quiz</span>' : '';
var url = buildModuleUrl(m);
html += '<a href="'+url+'" class="mod-card" style="border-color:#e8833a;border-width:2px;margin-bottom:.5rem">';
html += '<div class="mod-img" style="height:80px;background:linear-gradient(135deg,#e8833a,#f4a261)"><span style="font-size:2.5rem">'+(m.icon||'📚')+'</span></div>';
html += '<div class="mod-body">';
html += '<h3 style="color:#e8833a">'+m.icon+' '+m.name+(m.forcedLevel ? ' — Stufe '+m.forcedLevel : '')+'</h3>';
html += '<p>Auftrag von deiner Lehrperson'+due+quiz+'</p>';
html += '<span class="mod-status" style="background:#fde8d0;color:#e8833a">▶ Jetzt starten</span>';
html += '</div></a>';
});
html += '</div>';
}
// Regulaere Module (free + locked)
html += '<div class="mod-grid">';
modList.filter(function(m) { return m.mode !== 'teacher_started'; }).forEach(function(m) {
var isLocked = m.mode === 'locked';
var cardImg = CARD_IMAGES[m.id];
var imgHtml = cardImg ? '<img src="'+cardImg+'" alt="">' : '<span style="font-size:3rem">'+(m.icon||'📚')+'</span>';
var url = buildModuleUrl(m);
if (isLocked) {
html += '<div class="mod-card locked">';
html += '<div class="mod-img" style="position:relative">'+imgHtml+'<div class="lock-icon">🔒</div></div>';
} else {
html += '<a href="'+url+'" class="mod-card">';
html += '<div class="mod-img">'+imgHtml+'</div>';
}
html += '<div class="mod-body">';
html += '<h3>'+(m.icon||'📚')+' '+m.name+'</h3>';
html += '<p>'+(m.desc||'')+'</p>';
if (isLocked) {
html += '<span class="mod-status locked-s">🔒 Gesperrt</span>';
html += '</div></div>';
} else {
html += '<span class="mod-status available">▶ Starten</span>';
html += '</div></a>';
}
});
html += '</div>';
document.getElementById('tab-modules').innerHTML = html;
}
// Modul-Beschreibungen kommen jetzt aus module_info.short_desc via API.
async function renderAchievements() {
// Lade echte Fortschrittsdaten
var progressData = {};
try {
var allProgress = await api('progress?student_id=0'); // eigener Fortschritt
if (Array.isArray(allProgress)) {
allProgress.forEach(function(p) { progressData[p.sim_id] = p; });
}
} catch(e) {}
var totalPlays = 0, totalStars = 0, totalXP = 0;
Object.values(progressData).forEach(function(p) {
totalPlays += parseInt(p.plays || 0);
totalStars = Math.max(totalStars, parseInt(p.best_stars || 0));
totalXP += parseInt(p.xp || 0);
});
var earned = [];
if (totalPlays >= 1) earned.push('first_sim');
if (Object.keys(progressData).length >= 3) earned.push('three_sims');
if (Object.keys(progressData).length >= 8) earned.push('all_sims');
if (totalStars >= 4) earned.push('high_score');
if (totalXP >= 100) earned.push('speed_run'); // repurposed as XP badge
var html = '<div style="margin-bottom:.8rem"><p style="font-size:.85rem;font-weight:700;color:#4a7c8a">Deine Erfolge</p>';
html += '<p style="font-size:.72rem;color:#8a8a8a">Spiele Simulationen und sammle Auszeichnungen.</p></div>';
html += '<div class="badge-grid">';
BADGES.forEach(function(b) {
var isEarned = earned.indexOf(b.id) !== -1;
html += '<div class="badge '+(isEarned ? 'earned' : 'locked-b')+'">';
html += '<div class="icon">'+b.icon+'</div>';
html += '<div class="name">'+b.name+'</div>';
html += '<div class="desc">'+b.desc+'</div>';
html += '</div>';
});
html += '</div>';
document.getElementById('tab-achievements').innerHTML = html;
}
async function renderProfile() {
if (!profile) {
document.getElementById('tab-profile').innerHTML = '<p style="color:#8a8a8a">Profil wird geladen…</p>';
return;
}
// Lade Fortschritt
var totalPlays = 0, totalXP = 0, maxLevel = 1;
try {
var prog = await api('progress?student_id=0');
if (Array.isArray(prog)) prog.forEach(function(p) {
totalPlays += parseInt(p.plays||0);
totalXP += parseInt(p.xp||0);
maxLevel = Math.max(maxLevel, parseInt(p.level||1));
});
} catch(e) {}
var html = '<div class="profile-card">';
html += '<div class="profile-avatar" onclick="location.href=\'profil.html\'">'+(profile.emoji_avatar || '🧑‍🎓')+'</div>';
html += '<div class="profile-name">'+(profile.display_name || profile.username)+'</div>';
html += '<div class="profile-class">Klasse '+(profile.class_name || '—')+'</div>';
html += '<div class="profile-stats">';
html += '<div class="profile-stat"><div class="n">'+totalPlays+'</div><div class="l">Simulationen</div></div>';
html += '<div class="profile-stat"><div class="n">'+totalXP+'</div><div class="l">XP</div></div>';
html += '<div class="profile-stat"><div class="n">Level '+maxLevel+'</div><div class="l">Höchstes Level</div></div>';
html += '</div></div>';
html += '<div style="text-align:center;margin-top:1rem"><a href="profil.html" class="tab" style="display:inline-block;padding:.5rem 1.2rem;background:#4a7c8a;color:#fff;border-radius:8px;text-decoration:none;font-size:.78rem;font-weight:700">Profil bearbeiten</a></div>';
document.getElementById('tab-profile').innerHTML = html;
}
init();
</script>
</body>
</html>