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>
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user