9a61f55cb1
- 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>
308 lines
15 KiB
HTML
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>
|