Files
geograsim/App/schueler.html
T
Adminator 9a61f55cb1 Atlas: Infrastruktur + Team-Konventionen + Sprachregel Lernarbeit
- Design-System (assets/css/design-system.css) mit 21 Komponenten,
  iPad-Responsive-Breakpoints, Touch-Ziele 36px, Music-Player,
  Glossar-Tooltips
- Templates (sims/template.html + student/teacher-Dashboard)
- Docs: module-interface.md (inkl. 4a Sprachregel, 4b Leichte Sprache,
  4c iPad), content-architecture.md, crash-recovery.md, music-registry.md
- Admin-Infrastruktur: admin-modules.html + api/admin-modules.php
  (Titel, Emoji, Bild, Status, Dauer, Alter pro Modul)
- Inbox-System: _inbox/README.md + _status.md fuer Atlas + Briefings
  an Klima, Glossar, Lehrplan, Fluss
- Zentrale SFX-Pipeline (scripts/generate-sounds.py)
- DALL-E-Bilder: 8 Badges + 5 Glossar-Repraesentationsbilder (Querformat)
- Logo + Inter-Font lokal
- PHP-APIs: admin, glossar, levels, licenses, progress, waypoints,
  assignments, profile, tickets
- Spielsprache entfernt (admin-modules, admin-levels, schueler)
- Landing-Page-Bearbeitungen (Boote sichtbarer, Button-Hintergrund)

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

308 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 = [];
var MODULE_MAP = {
'sim-05': {name:'Klimasimulator 2D', href:'game.html', img:'assets/img/card-climate.png', icon:'🌡️'},
'sim-05-3d': {name:'Klimasimulator 3D', href:'game-3d.html', img:'assets/img/card-climate.png', icon:'🌊'},
'sim-07': {name:'Erdbeben & Plattentektonik', href:'erdbeben.html', img:'assets/img/card-tectonic.png', icon:'🌋'},
'sim-09': {name:'Energiewende-Planer', href:'energiemix.html', img:'assets/img/card-energy.png', icon:'⚡'},
'sim-10': {name:'Lieferketten-Planer', href:'lieferketten.html', img:'assets/img/card-globe.png', icon:'👕'},
'sim-11': {name:'Regenwald-Expedition', href:'regenwald.html', img:'assets/img/card-climate.png', icon:'🌴'},
'sim-12': {name:'Flussmanagement', href:'fluss.html', img:'assets/img/card-fluss.png', icon:'🏞️'},
'sim-13': {name:'Stadt & Raumplanung', href:'stadt.html', img:'assets/img/card-city.png', icon:'🏗️'},
'sim-14': {name:'Helikopter-Navigation', href:'heli-game', img:'assets/img/card-heli.png', icon:'🚁'},
};
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();
}
async function renderModules() {
var html = '';
// Aktive Aufgaben der Klasse laden
try {
var assignments = await api('assignments?student=1');
if (!assignments.error && assignments.length > 0) {
html += '<div style="margin-bottom:1rem">';
assignments.forEach(function(a) {
var m = MODULE_MAP[a.module_id];
if (!m) return;
var end = a.ends_at ? '<br><span style="font-size:.6rem;color:#8a8a8a">Zu erledigen bis ' + new Date(a.ends_at).toLocaleDateString('de-AT',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}) + '</span>' : '';
html += '<a href="'+m.href+'" 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+' — Level '+a.level+'</h3>';
html += '<p>Aufgabe von deiner Lehrperson'+end+'</p>';
html += '<span class="mod-status" style="background:#fde8d0;color:#e8833a">▶ Jetzt starten</span>';
html += '</div></a>';
});
html += '</div>';
}
} catch(e) {}
// Modul-Status laden
var modStatus = {};
try {
var mods = await api('modules?student=1');
if (!mods.error) mods.forEach(function(m) { modStatus[m.id] = m.mode; });
} catch(e) {}
// Alle Module
html += '<div class="mod-grid">';
var entries = Object.entries(MODULE_MAP);
entries.forEach(function(e) {
var id = e[0], m = e[1];
var mode = modStatus[id] || 'free'; // Default: frei wenn keine Klasse
var isLocked = mode === 'locked';
var imgHtml = m.img
? '<img src="'+m.img+'" alt="">'
: '<span style="font-size:3rem">'+m.icon+'</span>';
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="'+m.href+'" class="mod-card">';
html += '<div class="mod-img">'+imgHtml+'</div>';
}
html += '<div class="mod-body">';
html += '<h3>'+m.icon+' '+m.name+'</h3>';
html += '<p>'+getModuleDesc(id)+'</p>';
if (isLocked) {
html += '<span class="mod-status locked-s">🔒 Gesperrt</span>';
html += '</div></div>';
} else {
html += '<span class="mod-status available">▶ Spielen</span>';
html += '</div></a>';
}
// Level-Badge (wird asynchron gefüllt)
// Wird nach dem Rendern per API geladen
});
html += '</div>';
document.getElementById('tab-modules').innerHTML = html;
}
function getModuleDesc(id) {
var descs = {
'sim-05': 'Inselstaat durch die Klimakrise führen (2D-Ansicht)',
'sim-05-3d': 'Inselstaat durch die Klimakrise führen (3D)',
'sim-07': '50 Jahre Stadtplanung in einer Erdbebenzone',
'sim-09': 'Deine Region bis 2050 auf Erneuerbare umstellen',
'sim-10': 'Was kostet dein T-Shirt wirklich?',
'sim-11': 'Mit dem Boot den Fluss hoch — Tiere und Pflanzen entdecken',
'sim-12': 'Deiche, Renaturierung und Bewässerung managen',
'sim-13': 'Plane eine Stadt mit Wohngebieten und Infrastruktur',
'sim-14': 'Steuere einen Rettungshubschrauber durch Österreich',
};
return descs[id] || '';
}
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>