33fb423967
- Neuer API-Endpoint /api/live (heartbeat / spectator) + DB-Tabelle live_sessions + class_modules.paused-Spalte - Plattform-Live-Client (assets/js/live-client.js): Heartbeat, Pause-Overlay, Spectator-Mode bei ?view=teacher - ggs_inject_live() in _scripts.php als Helper, in alle 11 Sim-Wrapper integriert - Lehrer-Cockpit: tabellarische Klassen-Live-Ansicht pro Sim, Pause-Toggle, blinkender Tab-Indikator wenn aktive Sessions - Submit-Bug erschlagen: progress.php hat jetzt submit_assessment- und reflection-Action; saves.php akzeptiert beide Key-Konventionen; alle Direkt-Pfad-API-Files mit require_once-Bootstrap - Avatar-Default: zufaelliger DALL-E-Avatar fuer neue Schueler:innen - Admin-Styleguide-Seite mit Bildstil, Farb-Tokens, Layout, iPad-Pattern - Klima 2D + 3D: GGS_LIVE_STATE-Hook, Klima 2D zusaetzlich skipResume fuer Klassenaufgaben-Reset - Test-Lehrer Jakob + 3 Schueler:innen lokal + auf Live angelegt - Rundbriefe an alle Sim-Instanzen + Submit-Briefe an sonnensystem, busfahrt, energiemanager + Glossar-Anfrage zu Grundriess - Status-Uebergabe in _inbox/zentrale/_status.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
376 lines
20 KiB
HTML
376 lines
20 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:.15rem .6rem .15rem .15rem;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;width:28px;height:28px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:#fff;overflow:hidden}
|
||
.topbar .user-pill .emoji img{width:100%;height:100%;object-fit:cover;display:block}
|
||
.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(280px,1fr));gap:1rem}
|
||
.mod-card{background:#fff;border-radius:12px;overflow:hidden;border:1.5px solid rgba(0,0,0,.05);transition:all .25s;display:flex;flex-direction:column}
|
||
.mod-card.spielbar{cursor:default}
|
||
.mod-card.locked{opacity:.55}
|
||
.mod-card.coming{opacity:.7}
|
||
.mod-card.coming .mod-img{filter:grayscale(.5)}
|
||
.coming-icon{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:1rem;background:rgba(255,255,255,.92);border-radius:8px;padding:.3rem .55rem;font-weight:700;color:#8a6a3a;white-space:nowrap}
|
||
.coming-divider{margin:1.4rem 0 .8rem;font-size:.74rem;font-weight:700;color:#8a8a8a;text-transform:uppercase;letter-spacing:.06em;padding:.4rem .2rem;border-top:1px solid rgba(0,0,0,.06)}
|
||
.mod-img{height:210px;background:linear-gradient(135deg,#dae8ec,#c8dce2);display:flex;align-items:center;justify-content:center;font-size:2.5rem;position:relative;overflow:hidden}
|
||
.mod-img img{width:100%;height:100%;object-fit:cover;transition:transform .35s}
|
||
.mod-card.spielbar:hover .mod-img img{transform:scale(1.04)}
|
||
.mod-body{padding:.7rem .8rem .9rem;flex:1;display:flex;flex-direction:column}
|
||
.mod-body h3{font-size:.92rem;font-weight:700;margin-bottom:.25rem;line-height:1.2}
|
||
.mod-body p{font-size:.72rem;color:#4a4a4a;margin-bottom:.6rem;line-height:1.4;flex:1}
|
||
.mod-actions{display:flex;flex-wrap:wrap;gap:.35rem;margin-top:auto}
|
||
.mod-btn{display:inline-flex;align-items:center;gap:.25rem;padding:.42rem .7rem;border-radius:7px;font-size:.7rem;font-weight:600;text-decoration:none;border:1.5px solid rgba(0,0,0,.08);background:#f7f6f3;color:#4a4a4a;transition:all .15s;min-height:32px;white-space:nowrap}
|
||
.mod-btn:hover{background:#fff;border-color:rgba(74,124,138,.4);color:#1a1a1a}
|
||
.mod-btn.primary{background:#4a7c8a;color:#fff;border-color:#4a7c8a;flex:1;justify-content:center}
|
||
.mod-btn.primary:hover{background:#3d6671;color:#fff}
|
||
.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}
|
||
/* Erfolgs-Liste pro Modul */
|
||
.ach-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:.6rem;margin-bottom:1.2rem}
|
||
.ach-summary .card{background:#fff;border-radius:10px;padding:.9rem .8rem;text-align:center;border:1.5px solid rgba(0,0,0,.05)}
|
||
.ach-summary .n{font-size:1.6rem;font-weight:900;color:#4a7c8a;line-height:1}
|
||
.ach-summary .l{font-size:.62rem;color:#8a8a8a;margin-top:.25rem;text-transform:uppercase;letter-spacing:.04em}
|
||
.ach-modules{display:flex;flex-direction:column;gap:.5rem;margin-bottom:1.4rem}
|
||
.ach-mod{background:#fff;border-radius:10px;padding:.7rem .9rem;display:flex;align-items:center;gap:.7rem;border:1.5px solid rgba(0,0,0,.05)}
|
||
.ach-mod .ic{font-size:1.5rem;width:36px;text-align:center;flex-shrink:0}
|
||
.ach-mod .info{flex:1;min-width:0}
|
||
.ach-mod .info h4{font-size:.85rem;font-weight:700;margin:0 0 .15rem 0}
|
||
.ach-mod .info .meta{font-size:.65rem;color:#8a8a8a}
|
||
.ach-mod .stats{display:flex;gap:.5rem;flex-shrink:0}
|
||
.ach-mod .stat-pill{background:#f0eeea;color:#4a4a4a;padding:.2rem .55rem;border-radius:6px;font-size:.62rem;font-weight:700}
|
||
.ach-mod .stat-pill.stars{background:#fff4d6;color:#a37800}
|
||
.ach-mod .stat-pill.lvl{background:#dae8ec;color:#4a7c8a}
|
||
.ach-empty{background:#fff;border-radius:10px;padding:1.5rem 1rem;text-align:center;border:1.5px dashed rgba(0,0,0,.1);color:#8a8a8a;font-size:.78rem}
|
||
|
||
/* 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">
|
||
<a href="schueler" aria-label="Cockpit"><img src="assets/img/geograsim_t.svg" alt="GeoGraSim" class="logo"></a>
|
||
<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, desc). Bild-Pfad ist ein Dateiname wie "card-farmer.png" und wird
|
||
// gegen "assets/img/" relativ zur App-Basis aufgeloest.
|
||
|
||
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) {
|
||
var v = profile.emoji_avatar;
|
||
var pillEmoji = document.querySelector('#user-pill .emoji');
|
||
if (typeof v === 'string' && v.indexOf('avatar:') === 0) {
|
||
var slug = v.substring(7);
|
||
pillEmoji.innerHTML = '<img src="assets/img/avatars/avatar-' + slug + '.png" alt="">';
|
||
} else {
|
||
pillEmoji.textContent = v;
|
||
}
|
||
}
|
||
|
||
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 — erledigte Auftraege fallen automatisch
|
||
// in die regulaere Modul-Liste, damit der Auftrag aus dem Cockpit verschwindet,
|
||
// das Modul aber spielbar bleibt.
|
||
var teacherStarted = modList.filter(function(m) { return m.mode === 'teacher_started' && !m.assignmentCompleted; });
|
||
if (teacherStarted.length > 0) {
|
||
html += '<div style="margin-bottom:1rem">';
|
||
html += '<p style="font-size:.78rem;font-weight:700;color:#e8833a;margin-bottom:.4rem">📋 Aufträge 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: spielbar zuerst, "in Kürze" am Ende.
|
||
function renderModCard(m) {
|
||
var isComing = m.mode === 'coming_soon';
|
||
var isLocked = m.mode === 'locked';
|
||
var isSpielbar = !isComing && !isLocked;
|
||
var cardImg = m.card_image ? 'assets/img/' + m.card_image : null;
|
||
var imgHtml = cardImg ? '<img src="'+cardImg+'" alt="">' : '<span style="font-size:3rem">'+(m.icon||'📚')+'</span>';
|
||
var url = buildModuleUrl(m);
|
||
var moduleId = m.id;
|
||
var infoHref = 'modul-' + moduleId;
|
||
var glossarHref = 'glossar?module=' + moduleId;
|
||
|
||
var cls = isComing ? 'coming' : (isLocked ? 'locked' : 'spielbar');
|
||
var s = '<div class="mod-card '+cls+'">';
|
||
var overlay = isComing ? '<div class="coming-icon">🚧 In Kürze</div>'
|
||
: isLocked ? '<div class="lock-icon">🔒</div>'
|
||
: '';
|
||
s += '<div class="mod-img">'+imgHtml+overlay+'</div>';
|
||
s += '<div class="mod-body">';
|
||
s += '<h3>'+(m.icon||'📚')+' '+m.name+'</h3>';
|
||
s += '<p>'+(m.desc||'')+'</p>';
|
||
s += '<div class="mod-actions">';
|
||
if (isSpielbar) s += '<a href="'+url+'" class="mod-btn primary">▶ Starten</a>';
|
||
s += '<a href="'+infoHref+'" class="mod-btn" title="Mehr Infos">📖 Infos</a>';
|
||
if (!isComing) s += '<a href="'+glossarHref+'" class="mod-btn" title="Glossar zu diesem Modul">📚 Glossar</a>';
|
||
s += '</div>';
|
||
s += '</div></div>';
|
||
return s;
|
||
}
|
||
|
||
// Regulaere Liste enthaelt alles ausser noch offene Lehrer-Auftraege.
|
||
// Erledigte Auftraege erscheinen hier als spielbar, damit der Schueler sie
|
||
// freiwillig wiederholen kann.
|
||
var regular = modList.filter(function(m) { return m.mode !== 'teacher_started' || m.assignmentCompleted; });
|
||
var spielbar = regular.filter(function(m) { return m.mode !== 'coming_soon'; });
|
||
var coming = regular.filter(function(m) { return m.mode === 'coming_soon'; });
|
||
|
||
html += '<div class="mod-grid">';
|
||
spielbar.forEach(function(m) { html += renderModCard(m); });
|
||
html += '</div>';
|
||
|
||
if (coming.length > 0) {
|
||
html += '<div class="coming-divider">In Kürze verfügbar</div>';
|
||
html += '<div class="mod-grid">';
|
||
coming.forEach(function(m) { html += renderModCard(m); });
|
||
html += '</div>';
|
||
}
|
||
|
||
document.getElementById('tab-modules').innerHTML = html;
|
||
}
|
||
|
||
// Modul-Beschreibungen kommen jetzt aus module_info.short_desc via API.
|
||
|
||
async function renderAchievements() {
|
||
var allProgress = [];
|
||
try {
|
||
var resp = await api('progress?student_id=0');
|
||
if (Array.isArray(resp)) allProgress = resp;
|
||
} catch(e) {}
|
||
|
||
var totalPlays = 0, totalXP = 0, bestStarsOverall = 0;
|
||
allProgress.forEach(function(p) {
|
||
totalPlays += parseInt(p.plays || 0);
|
||
totalXP += parseInt(p.xp || 0);
|
||
bestStarsOverall = Math.max(bestStarsOverall, parseInt(p.best_stars || 0));
|
||
});
|
||
var simCount = allProgress.length;
|
||
|
||
var earned = [];
|
||
if (totalPlays >= 1) earned.push('first_sim');
|
||
if (simCount >= 3) earned.push('three_sims');
|
||
if (simCount >= 8) earned.push('all_sims');
|
||
if (bestStarsOverall >= 4) earned.push('high_score');
|
||
if (totalXP >= 100) earned.push('speed_run');
|
||
|
||
var html = '';
|
||
|
||
// Top-Kennzahlen
|
||
html += '<div class="ach-summary">';
|
||
html += '<div class="card"><div class="n">'+simCount+'</div><div class="l">Module begonnen</div></div>';
|
||
html += '<div class="card"><div class="n">'+totalPlays+'</div><div class="l">Durchgänge</div></div>';
|
||
html += '<div class="card"><div class="n">'+totalXP+'</div><div class="l">XP gesamt</div></div>';
|
||
html += '</div>';
|
||
|
||
// Modul-Erfolge-Liste
|
||
html += '<p style="font-size:.85rem;font-weight:700;color:#4a7c8a;margin-bottom:.5rem">Was du in den Modulen geschafft hast</p>';
|
||
if (allProgress.length === 0) {
|
||
html += '<div class="ach-empty">Noch keine Module gespielt. Geh auf <strong>Meine Module</strong> und starte einen Durchgang.</div>';
|
||
} else {
|
||
html += '<div class="ach-modules">';
|
||
allProgress.forEach(function(p) {
|
||
var stars = '★'.repeat(p.best_stars || 0) + '☆'.repeat(Math.max(0, 5 - (p.best_stars || 0)));
|
||
var last = p.last_played_at ? new Date(p.last_played_at.replace(' ','T')+'Z').toLocaleDateString('de-AT', {day:'2-digit',month:'2-digit',year:'2-digit'}) : '—';
|
||
html += '<div class="ach-mod">';
|
||
html += '<div class="ic">'+(p.icon || '📚')+'</div>';
|
||
html += '<div class="info"><h4>'+(p.title || p.sim_id)+'</h4>';
|
||
html += '<div class="meta">'+p.plays+'× gespielt · '+p.xp+' XP · zuletzt '+last+'</div></div>';
|
||
html += '<div class="stats">';
|
||
html += '<span class="stat-pill stars" title="Beste Sterne">'+stars+'</span>';
|
||
html += '<span class="stat-pill lvl">Lvl '+(p.level || 1)+'</span>';
|
||
html += '</div></div>';
|
||
});
|
||
html += '</div>';
|
||
}
|
||
|
||
// Auszeichnungen / Badges
|
||
html += '<p style="font-size:.85rem;font-weight:700;color:#4a7c8a;margin-bottom:.5rem">Auszeichnungen</p>';
|
||
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">';
|
||
var pAv = profile.emoji_avatar || '🧑🎓';
|
||
var pAvHtml = (typeof pAv === 'string' && pAv.indexOf('avatar:') === 0)
|
||
? '<img src="assets/img/avatars/avatar-' + pAv.substring(7) + '.png" alt="" style="width:100%;height:100%;border-radius:50%;object-fit:cover">'
|
||
: pAv;
|
||
html += '<div class="profile-avatar" onclick="location.href=\'profil.html\'" style="width:80px;height:80px;border-radius:50%;background:#dae8ec;display:flex;align-items:center;justify-content:center;margin:0 auto;overflow:hidden">'+pAvHtml+'</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>
|
||
<script src="assets/js/version-tag.js" defer></script>
|
||
</body>
|
||
</html>
|