fecd8bb9c9
562 Ersetzungen in 50 Dateien: Halbgeviertstrich zwischen zwei Ziffern (= "bis"-Bereich) bekommt Leerzeichen davor/danach (3–4 -> 3 – 4). Regex /([0-9])\s*–\s*(?=[0-9])/ auf Rohtext (format-erhaltend, CRLF/Einrueckung unveraendert), Bindestriche in Woertern und Minuszeichen unangetastet, alle JSON weiter gueltig. Energiemanager-Zeitbloecke sind Display-Labels (kein Logik-Key) -> safe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
527 lines
32 KiB
HTML
527 lines
32 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 — Profil</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;min-height:100vh}
|
||
/* Topbar 1:1 wie schueler.html — gleiche Kopfzeile in der Schüler-Ansicht */
|
||
.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:600px;margin:2rem auto;padding:0 1rem}
|
||
/* Tab-Navigation 1:1 wie schueler.html — Profil ist hier aktiv. */
|
||
.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;text-decoration:none}
|
||
.tab.on{background:#4a7c8a;color:#fff}
|
||
.tab:hover:not(.on){background:#f0eee6}
|
||
.tabs.hidden{display:none}
|
||
.card{background:#fff;border-radius:12px;padding:1.2rem;box-shadow:0 2px 8px rgba(0,0,0,.04);border:1px solid rgba(0,0,0,.04);margin-bottom:1rem}
|
||
.card-h{font-size:.62rem;font-weight:700;color:#4a7c8a;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.6rem}
|
||
.avatar-section{text-align:center;padding:1rem 0}
|
||
.avatar-current{width:100px;height:100px;border-radius:50%;cursor:pointer;transition:transform .2s;display:inline-block;background:#dae8ec;font-size:3.5rem;line-height:100px;text-align:center;overflow:hidden}
|
||
.avatar-current img{width:100%;height:100%;object-fit:cover;display:block}
|
||
.avatar-current:hover{transform:scale(1.08)}
|
||
.avatar-name{font-size:1.1rem;font-weight:700;margin-top:.5rem}
|
||
.avatar-role{font-size:.72rem;color:#8a8a8a;margin-top:.1rem}
|
||
.avatar-picker{display:none;grid-template-columns:repeat(6,1fr);gap:.6rem .4rem;margin-top:1rem;padding:.6rem;background:#f7f6f3;border-radius:10px;max-height:520px;overflow-y:auto}
|
||
.avatar-picker.open{display:grid}
|
||
.avatar-picker-section{grid-column:1/-1;font-size:.66rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;margin:.6rem .2rem .15rem;padding-bottom:.2rem;border-bottom:1px solid rgba(0,0,0,.08)}
|
||
.avatar-picker-section:first-child{margin-top:0}
|
||
.avatar-opt{cursor:pointer;display:flex;flex-direction:column;align-items:center;text-align:center;gap:.25rem;transition:transform .15s}
|
||
.avatar-opt > img{width:100%;aspect-ratio:1;border-radius:50%;object-fit:cover;border:2.5px solid transparent;background:#dae8ec;display:block}
|
||
.avatar-opt:hover{transform:scale(1.06)}
|
||
.avatar-opt:hover > img{border-color:#9bbac0}
|
||
.avatar-opt.active > img{border-color:#4a7c8a;box-shadow:0 0 0 2px #4a7c8a}
|
||
.avatar-opt-name{font-size:.58rem;line-height:1.2;color:#4a4a4a;font-weight:600;hyphens:auto;word-break:break-word}
|
||
.avatar-opt.active .avatar-opt-name{color:#1f4b37}
|
||
.field{margin-bottom:.8rem}
|
||
.field label{display:block;font-size:.68rem;font-weight:600;color:#4a4a4a;margin-bottom:.15rem}
|
||
.field input{width:100%;padding:.45rem .65rem;border:1.5px solid rgba(0,0,0,.1);border-radius:7px;font-size:.8rem;font-family:inherit;outline:none;transition:border .15s}
|
||
.field input:focus{border-color:#4a7c8a}
|
||
.field input:disabled{background:#f7f6f3;color:#8a8a8a}
|
||
.btn{padding:.45rem .9rem;border:none;border-radius:7px;background:#4a7c8a;color:#fff;font-size:.75rem;font-weight:700;cursor:pointer;transition:background .15s}
|
||
.btn:hover{background:#3a6470}
|
||
.btn-danger{background:#c07a6b}
|
||
.btn-danger:hover{background:#a05a4b}
|
||
.msg{font-size:.72rem;padding:.35rem .6rem;border-radius:6px;margin-top:.5rem;display:none}
|
||
.msg.ok{display:block;background:#dceadd;color:#5a8a5e}
|
||
.msg.err{display:block;background:#f2ddd8;color:#c07a6b}
|
||
.info-row{display:flex;justify-content:space-between;padding:.3rem 0;border-bottom:1px solid rgba(0,0,0,.03);font-size:.78rem}
|
||
.info-row:last-child{border:none}
|
||
.info-label{color:#8a8a8a;font-weight:500}
|
||
.info-val{font-weight:600}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="topbar">
|
||
<a href="schueler.html" id="logo-link" 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" id="user-emoji">🧑🎓</span><span id="user-name">…</span></a>
|
||
<button class="btn-sm" onclick="logout()">Abmelden</button>
|
||
</div>
|
||
|
||
<div class="wrap" id="content">
|
||
<!-- Tab-Navigation für Schüler*innen — Module / Erfolge / Profil. -->
|
||
<nav class="tabs hidden" id="student-tabs" aria-label="Schüler-Navigation">
|
||
<a class="tab" href="schueler.html" id="tab-link-modules">Meine Module</a>
|
||
<a class="tab" href="schueler.html#achievements" id="tab-link-achievements">Meine Erfolge</a>
|
||
<a class="tab on" href="profil.html" id="tab-link-profile">Profil</a>
|
||
</nav>
|
||
<!-- Tab-Navigation für Lehrkräfte — spiegelt das Cockpit-Menü. Klick auf
|
||
Cockpit-Tab landet zurück in teacher.html mit Hash → richtiger Tab aktiv. -->
|
||
<nav class="tabs hidden" id="teacher-tabs" aria-label="Lehrer-Navigation">
|
||
<a class="tab" href="teacher.html">Übersicht</a>
|
||
<a class="tab" href="teacher.html#modules">Modulfreigabe</a>
|
||
<a class="tab" href="teacher.html#students">Klasse</a>
|
||
<a class="tab" href="teacher.html#live">Live</a>
|
||
<a class="tab" href="teacher.html#results">Ergebnisse</a>
|
||
<a class="tab" href="teacher.html#licenses">Lizenzen</a>
|
||
<a class="tab on" href="profil.html">Profil</a>
|
||
</nav>
|
||
<div id="profile-content">
|
||
<div style="text-align:center;padding:3rem;color:#8a8a8a">Laden...</div>
|
||
</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'; } }
|
||
// Bild-Avatare im Plattform-Bildstil (1024×1024 PNG, kreisrund maskiert).
|
||
// Slugs entsprechen Datei-Namen: avatar-<slug>.png unter assets/img/avatars/.
|
||
// Pro Avatar: name = "Vorname/Bezeichnung — Rolle". Picker zeigt das als
|
||
// Tooltip + Label. Bilder, die noch nicht generiert sind, fallen per onerror
|
||
// auf einen farbigen Initial-Kreis im Bildstil zurück.
|
||
var AVATAR_GROUPS = [
|
||
{ name: 'In der Geo-Welt', color: '#1f4b37', items: [
|
||
{slug:'pilotin', name:'Yara, Pilotin', emoji:'👩✈️'},
|
||
{slug:'pilot', name:'Liu, Pilot', emoji:'👨✈️'},
|
||
{slug:'kartografin', name:'Annika, Kartografin', emoji:'🗺️'},
|
||
{slug:'geologe', name:'Marco, Geologe', emoji:'⛏️'},
|
||
{slug:'meteorologin', name:'Aïcha, Meteorologin', emoji:'⛅'},
|
||
{slug:'stadtplaner', name:'Henrik, Stadtplaner', emoji:'🏙️'},
|
||
{slug:'forscherin', name:'Mira, Feldforscherin', emoji:'🔬'},
|
||
{slug:'forscher', name:'David, Forscher', emoji:'🧑🔬'},
|
||
{slug:'taucherin', name:'Naomi, Taucherin', emoji:'🤿'},
|
||
{slug:'astronaut', name:'Carlos, Astronaut', emoji:'👨🚀'},
|
||
{slug:'ozeanographin', name:'Linnea, Ozeanographin', emoji:'🌊'},
|
||
{slug:'tropenbiologe', name:'Kwame, Tropenbiologe', emoji:'🦋'},
|
||
]},
|
||
{ name: 'Cool & lustig', color: '#e8a247', items: [
|
||
{slug:'coole-girl', name:'Zoe, cool', emoji:'😎'},
|
||
{slug:'coole-boy', name:'Mateo, cool', emoji:'🕶️'},
|
||
{slug:'lustige-girl', name:'Lina, lustig', emoji:'🤪'},
|
||
{slug:'lustige-boy', name:'Felix, lustig', emoji:'😜'},
|
||
{slug:'nerd-girl', name:'Sophie, Nerd', emoji:'🤓'},
|
||
{slug:'sportlich-boy', name:'Diego, sportlich', emoji:'⚽'},
|
||
{slug:'kuenstlerin', name:'Mia, Künstlerin', emoji:'🎨'},
|
||
{slug:'musiker', name:'Jamal, Musiker', emoji:'🎸'},
|
||
{slug:'gaertnerin', name:'Hanna, Gärtnerin', emoji:'🌱'},
|
||
{slug:'abenteurer', name:'Oskar, Abenteurer', emoji:'🎒'},
|
||
{slug:'skaterin', name:'Reni, Skaterin', emoji:'🛹'},
|
||
{slug:'dj', name:'Kofi, DJ', emoji:'🎧'},
|
||
]},
|
||
{ name: 'Tier-Begleiter', color: '#4a7c4e', items: [
|
||
{slug:'fuchs-explorer', name:'Rufus, Fuchs-Explorer', emoji:'🦊'},
|
||
{slug:'eule-prof', name:'Hedwig, Eulen-Professorin', emoji:'🦉'},
|
||
{slug:'baer-foerster', name:'Bruno, Bären-Förster', emoji:'🐻'},
|
||
{slug:'pinguin-polar', name:'Pingu, Polar-Pinguin', emoji:'🐧'},
|
||
{slug:'affe-tropen', name:'Coco, Tropen-Affe', emoji:'🐵'},
|
||
{slug:'delfin-meer', name:'Delphi, Meeres-Delfin', emoji:'🐬'},
|
||
{slug:'adler-pilot', name:'Aron, Adler-Pilot', emoji:'🦅'},
|
||
{slug:'schildkroete-w', name:'Schildi, Weltreise-Schildkröte', emoji:'🐢'},
|
||
{slug:'katze-stadt', name:'Minka, Stadt-Katze', emoji:'🐱'},
|
||
{slug:'biene-natur', name:'Bea, Natur-Biene', emoji:'🐝'},
|
||
{slug:'wolf-tundra', name:'Yuki, Tundra-Wolf', emoji:'🐺'},
|
||
{slug:'krake-tiefsee', name:'Otto, Tiefsee-Krake', emoji:'🐙'},
|
||
]},
|
||
{ name: 'Aliens erforschen die Welt', color: '#7c3aed', items: [
|
||
{slug:'alien-geologin', name:'Zorblax, Stein-Forscherin von Geo-7', emoji:'👽'},
|
||
{slug:'alien-ozean', name:'Aquana, Ozean-Spähterin von Aqualon', emoji:'🛸'},
|
||
{slug:'alien-wetter', name:'Tempest, Wetter-Beobachter von Nimbus', emoji:'⛈️'},
|
||
{slug:'alien-stadt', name:'Urbanix, Stadt-Analyst von Metropolis-9', emoji:'🌃'},
|
||
{slug:'alien-tiere', name:'Faunor, Tier-Studienleiter von Zoonia', emoji:'🐾'},
|
||
{slug:'alien-pflanzen', name:'Floranth, Pflanzen-Diplomat von Verdantis', emoji:'🌿'},
|
||
{slug:'alien-vulkan', name:'Magmara, Vulkan-Sondiererin von Pyrox', emoji:'🌋'},
|
||
{slug:'alien-arktis', name:'Glacium, Eis-Späher von Kryonia', emoji:'🧊'},
|
||
{slug:'alien-wueste', name:'Solara, Wüsten-Forscherin von Helios', emoji:'🏜️'},
|
||
{slug:'alien-regenwald', name:'Veridia, Regenwald-Späherin von Chlora', emoji:'🌳'},
|
||
{slug:'alien-berg', name:'Altimos, Berg-Vermesser von Alpinis', emoji:'🏔️'},
|
||
{slug:'alien-meer', name:'Marinus, Meeres-Erkunder von Pelagos', emoji:'🌊'},
|
||
]},
|
||
{ name: 'Berühmte Forschungsmaschinen', color: '#0d47a1', items: [
|
||
{slug:'bathyscaph-trieste', name:'Trieste — Tauchboot, 1960 Marianengraben (Piccard/Walsh)', emoji:'🛥️'},
|
||
{slug:'zeppelin-graf', name:'Graf Zeppelin — Welt-Umrundung 1929', emoji:'🎈'},
|
||
{slug:'ballon-charliere', name:'Charlière — erster Wasserstoff-Ballon 1783', emoji:'🎈'},
|
||
{slug:'polar-fram', name:'Fram — Nansens Polar-Schiff 1893', emoji:'⛵'},
|
||
{slug:'dampfschiff-beagle', name:'HMS Beagle — Darwins Forschungsschiff 1831', emoji:'🚢'},
|
||
{slug:'tauchglocke-halley', name:'Halleysche Tauchglocke — 1690 erster Mensch unter Wasser', emoji:'🛎️'},
|
||
{slug:'sonde-voyager', name:'Voyager 1 — fliegt seit 1977 ins All', emoji:'🛰️'},
|
||
{slug:'eisbohrer-vostok', name:'Vostok-Eisbohrkern — 420.000 Jahre Klima', emoji:'🧊'},
|
||
{slug:'rover-curiosity', name:'Curiosity — NASA Mars-Rover seit 2012', emoji:'🤖'},
|
||
{slug:'satellit-landsat', name:'Landsat — Erdbeobachtung seit 1972', emoji:'🛰️'},
|
||
{slug:'forschungsschiff-meteor', name:'FS Meteor — deutsches Forschungsschiff', emoji:'⚓'},
|
||
{slug:'wettersatellit-meteosat', name:'Meteosat — Wettersatellit über Europa', emoji:'📡'},
|
||
]},
|
||
{ name: 'Berühmte Forscherinnen und Forscher', color: '#8d6e63', items: [
|
||
{slug:'humboldt-alexander', name:'Alexander von Humboldt (1769 – 1859) — Universalgelehrter, Südamerika', emoji:'🌿'},
|
||
{slug:'mercator-gerhard', name:'Gerhard Mercator (1512 – 1594) — Kartograph, Welt-Karte', emoji:'🗺️'},
|
||
{slug:'cook-james', name:'James Cook (1728 – 1779) — Pazifik-Entdecker', emoji:'⚓'},
|
||
{slug:'polo-marco', name:'Marco Polo (1254 – 1324) — Reise nach China', emoji:'🐫'},
|
||
{slug:'ibn-battuta', name:'Ibn Battuta (1304 – 1369) — 120.000 km durch die Welt', emoji:'🕌'},
|
||
{slug:'nansen-fridtjof', name:'Fridtjof Nansen (1861 – 1930) — Polarforscher, Nobelpreis', emoji:'🧭'},
|
||
{slug:'piccard-auguste', name:'Auguste Piccard (1884 – 1962) — Stratosphäre + Tiefsee', emoji:'🎈'},
|
||
{slug:'amundsen-roald', name:'Roald Amundsen (1872 – 1928) — erster am Südpol 1911', emoji:'🐧'},
|
||
{slug:'earhart-amelia', name:'Amelia Earhart (1897 – 1937) — Pilotin, Atlantik-Überquerung', emoji:'✈️'},
|
||
{slug:'earle-sylvia', name:'Sylvia Earle (*1935) — Meeresbiologin, „Her Deepness"', emoji:'🐠'},
|
||
{slug:'anning-mary', name:'Mary Anning (1799 – 1847) — Paläontologin, erste Saurierfunde', emoji:'🦕'},
|
||
{slug:'carson-rachel', name:'Rachel Carson (1907 – 1964) — Umweltschutz-Pionierin', emoji:'🌳'},
|
||
]},
|
||
];
|
||
// Slug → Emoji-Lookup (fuer Fallback-Rendering)
|
||
var AVATAR_EMOJIS = {};
|
||
AVATAR_GROUPS.forEach(function(g){
|
||
(g.items || []).forEach(function(it){ if (it.emoji) AVATAR_EMOJIS[it.slug] = it.emoji; });
|
||
});
|
||
// Flach-Liste aller Slugs (für Validierung/Ausgangsdaten)
|
||
var ALL_AVATAR_SLUGS = AVATAR_GROUPS.reduce(function(a,g){
|
||
return a.concat((g.items || g.slugs || []).map(function(it){ return typeof it === 'string' ? it : it.slug; }));
|
||
}, []);
|
||
// Slug → Name (für Tooltip-Lookup beim aktuellen Avatar)
|
||
var AVATAR_NAMES = {};
|
||
AVATAR_GROUPS.forEach(function(g){
|
||
(g.items || []).forEach(function(it){ AVATAR_NAMES[it.slug] = it.name; });
|
||
});
|
||
|
||
// Stil-konformer Fallback wenn das PNG (noch) nicht existiert — Emoji im
|
||
// Farbkreis statt nur Initial-Buchstabe (deutlich erkennbarer).
|
||
// AVATAR_EMOJIS wird unten beim Erstellen der Gruppen befuellt.
|
||
function makeAvatarFallback(slug, color) {
|
||
var emoji = (typeof AVATAR_EMOJIS !== 'undefined' && AVATAR_EMOJIS[slug]) || '';
|
||
if (!emoji) {
|
||
// Fallback im Fallback: Initial-Buchstabe (für unbekannte Slugs).
|
||
emoji = (slug || '?').replace(/^(alien|avatar)-?/,'').replace(/[-_].*/,'').charAt(0).toUpperCase() || '?';
|
||
}
|
||
var svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">'
|
||
+ '<rect width="100" height="100" fill="' + (color||'#4a7c8a') + '"/>'
|
||
+ '<text x="50" y="68" font-size="56" text-anchor="middle" '
|
||
+ 'dominant-baseline="middle">' + emoji + '</text></svg>';
|
||
return 'data:image/svg+xml;utf8,' + encodeURIComponent(svg);
|
||
}
|
||
|
||
// Klassische Emojis als Fallback (Lehrkräfte sehen sie am Anfang noch).
|
||
var EMOJI_OPTIONS = ['🧑🎓','👩🎓','👨🎓','🧑🏫','👩🏫','👨🏫','🦊','🐱','🐶','🐻','🐼','🦁','🐧','🦉','🌍','🚀'];
|
||
|
||
function escHtml(s) { return String(s==null?'':s).replace(/[&<>"]/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"'}[c]; }); }
|
||
function escAttr(s) { return escHtml(s).replace(/'/g, '''); }
|
||
// Thumb (256x256 webp, ~10 KB) ist die Standard-URL fuer Listen/Picker/Pille.
|
||
// Fuer die grosse Profil-Vorschau gibt's avatarFullUrl (1024 PNG).
|
||
function avatarThumbUrl(slug) { return BASE + '/assets/img/avatars/thumbs/avatar-' + slug + '.webp'; }
|
||
function avatarFullUrl(slug) { return BASE + '/assets/img/avatars/avatar-' + slug + '.png'; }
|
||
function avatarUrl(slug) { return avatarThumbUrl(slug); } // alias for backward compat
|
||
function isAvatarSlug(v) { return typeof v === 'string' && v.indexOf('avatar:') === 0; }
|
||
function avatarValue(slug) { return 'avatar:' + slug; }
|
||
function avatarSlugFromValue(v) { return isAvatarSlug(v) ? v.substring(7) : null; }
|
||
// Welche Gruppe enthält den Slug? (für Fallback-Farbe)
|
||
function avatarColorForSlug(slug) {
|
||
for (var i = 0; i < AVATAR_GROUPS.length; i++) {
|
||
var items = AVATAR_GROUPS[i].items || [];
|
||
for (var j = 0; j < items.length; j++) if (items[j].slug === slug) return AVATAR_GROUPS[i].color || '#4a7c8a';
|
||
}
|
||
return '#4a7c8a';
|
||
}
|
||
function renderAvatar(value, fallback) {
|
||
var slug = avatarSlugFromValue(value);
|
||
if (slug) {
|
||
var fb = makeAvatarFallback(slug, avatarColorForSlug(slug));
|
||
return '<img src="' + avatarUrl(slug) + '" alt="" onerror="this.onerror=null;this.src=\'' + fb + '\'">';
|
||
}
|
||
return value || fallback || '🧑🎓';
|
||
}
|
||
|
||
async function api(endpoint, data) {
|
||
var opts = data !== undefined
|
||
? {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 = 'index.html'; });
|
||
}
|
||
|
||
async function loadProfile() {
|
||
var r = await api('profile');
|
||
if (r.role === 'guest' || !r.profile) {
|
||
location.href = 'login.html';
|
||
return;
|
||
}
|
||
|
||
// Logo → Cockpit (rollen-abhängig)
|
||
document.getElementById('logo-link').href = (r.role === 'student') ? 'schueler.html' : 'teacher.html';
|
||
|
||
// Rollen-spezifische Tab-Bar einblenden — analog zur jeweiligen Cockpit-Navi.
|
||
if (r.role === 'student') {
|
||
document.getElementById('student-tabs').classList.remove('hidden');
|
||
} else if (r.role === 'teacher') {
|
||
document.getElementById('teacher-tabs').classList.remove('hidden');
|
||
}
|
||
|
||
// User-Pill: Name + Avatar (gleiche Darstellung wie auf dem Cockpit)
|
||
document.getElementById('user-name').textContent = r.profile.display_name || r.profile.username || (r.role === 'student' ? 'Schüler*in' : 'Lehrperson');
|
||
var avatarVal = r.profile.emoji_avatar || (r.role === 'student' ? '🧑🎓' : '👩🏫');
|
||
var emojiEl = document.getElementById('user-emoji');
|
||
var slug = avatarSlugFromValue(avatarVal);
|
||
if (slug) {
|
||
var fb = makeAvatarFallback(slug, avatarColorForSlug(slug));
|
||
emojiEl.innerHTML = '<img src="' + avatarUrl(slug) + '" alt="" onerror="this.onerror=null;this.src=\'' + fb + '\'">';
|
||
} else {
|
||
emojiEl.innerHTML = '';
|
||
emojiEl.textContent = avatarVal;
|
||
}
|
||
|
||
if (r.role === 'student') {
|
||
renderStudentProfile(r.profile);
|
||
} else if (r.role === 'teacher') {
|
||
renderTeacherProfile(r.profile);
|
||
}
|
||
}
|
||
|
||
function renderStudentProfile(p) {
|
||
var html = '';
|
||
var current = p.emoji_avatar || '🧑🎓';
|
||
// Avatar + Name
|
||
html += '<div class="card"><div class="avatar-section">';
|
||
html += '<div class="avatar-current" id="avatar-btn" onclick="togglePicker()">' + renderAvatar(current, '🧑🎓') + '</div>';
|
||
html += '<div class="avatar-name">' + (p.display_name || p.username) + '</div>';
|
||
html += '<div class="avatar-role">Klasse ' + (p.class_name || '') + '</div>';
|
||
html += '<div class="avatar-picker" id="avatar-picker">';
|
||
AVATAR_GROUPS.forEach(function(g) {
|
||
html += '<div class="avatar-picker-section" style="color:' + (g.color||'#8a8a8a') + '">' + g.name + '</div>';
|
||
(g.items || []).forEach(function(it) {
|
||
var slug = it.slug, name = it.name || slug;
|
||
var val = avatarValue(slug);
|
||
var cls = val === current ? ' active' : '';
|
||
var fb = makeAvatarFallback(slug, g.color || '#4a7c8a');
|
||
html += '<div class="avatar-opt' + cls + '" data-val="' + val + '" title="' + escAttr(name) + '" onclick="selectAvatar(\'' + val + '\')">'
|
||
+ '<img src="' + avatarUrl(slug) + '" alt="' + escAttr(name) + '" onerror="this.onerror=null;this.src=\'' + fb + '\'">'
|
||
+ '<div class="avatar-opt-name">' + escHtml(name) + '</div>'
|
||
+ '</div>';
|
||
});
|
||
});
|
||
html += '</div></div></div>';
|
||
|
||
// Info
|
||
html += '<div class="card"><div class="card-h">Kontoinformationen</div>';
|
||
html += '<div class="info-row"><span class="info-label">Anzeigename</span><span class="info-val">' + (p.display_name || p.username) + '</span></div>';
|
||
html += '<div class="info-row"><span class="info-label">Benutzername</span><span class="info-val">' + p.username + '</span></div>';
|
||
html += '<div class="info-row"><span class="info-label">Klasse</span><span class="info-val">' + (p.class_name || '—') + '</span></div>';
|
||
html += '<div class="info-row"><span class="info-label">Klassencode</span><span class="info-val">' + (p.join_code || '—') + '</span></div>';
|
||
html += '<div class="info-row"><span class="info-label">Dabei seit</span><span class="info-val">' + new Date(p.created_at).toLocaleDateString('de-AT') + '</span></div>';
|
||
html += '<p style="font-size:.65rem;color:#8a8a8a;margin-top:.6rem">Anzeigename und Klasse vergibt die Lehrperson. Du kannst dein Profilbild ändern.</p>';
|
||
html += '</div>';
|
||
|
||
// === Meine Reflexionen — wird gleich nachgeladen ===
|
||
html += '<div class="card" id="student-reflections-card">'
|
||
+ '<div class="card-h">💭 Meine Reflexionen</div>'
|
||
+ '<p style="font-size:.78rem;color:#8a8a8a;margin-bottom:.6rem">Hier siehst du, was du am Ende deiner Durchgänge in den Simulationen notiert hast — deine eigenen Gedanken zu jedem Abschluss.</p>'
|
||
+ '<div id="student-reflections-list">Lade …</div>'
|
||
+ '</div>';
|
||
|
||
document.getElementById('profile-content').innerHTML = html;
|
||
_loadStudentReflections();
|
||
}
|
||
|
||
async function _loadStudentReflections() {
|
||
try {
|
||
var data = await api('profile?view=reflections');
|
||
var host = document.getElementById('student-reflections-list');
|
||
if (!host) return;
|
||
if (data.error) { host.innerHTML = '<p style="color:#c07a6b;font-size:.82rem">' + escHtml(data.error) + '</p>'; return; }
|
||
var items = data.items || [];
|
||
if (!items.length) {
|
||
host.innerHTML = '<p style="font-size:.82rem;color:#8a8a8a;font-style:italic">Du hast noch keine Reflexionen geschrieben. Am Ende eines Durchgangs wirst du gefragt, was dir am besten gefallen hat oder am schwierigsten war — deine Antwort erscheint dann hier.</p>';
|
||
return;
|
||
}
|
||
var html = '<div style="display:flex;flex-direction:column;gap:.6rem">';
|
||
items.forEach(function(it) {
|
||
var when = it.recordedAt ? new Date(it.recordedAt).toLocaleDateString('de-AT', {day:'2-digit',month:'2-digit',year:'numeric'}) : '';
|
||
html += '<div style="background:#fbfaf6;border:1px solid #e2ddd1;border-left:4px solid #c9913a;border-radius:8px;padding:.7rem .9rem">'
|
||
+ '<div style="display:flex;justify-content:space-between;font-size:.7rem;color:#8a8a8a;margin-bottom:.3rem">'
|
||
+ '<span><span style="background:#f0ede5;padding:.06rem .4rem;border-radius:99px;font-weight:600;color:#3a3a3a">' + escHtml(it.simId) + '</span>'
|
||
+ (it.level ? ' · Stufe ' + escHtml(it.level) : '') + '</span>'
|
||
+ '<span>' + escHtml(when) + '</span>'
|
||
+ '</div>'
|
||
+ '<div style="font-size:.78rem;color:#6a6a6a;font-style:italic;margin-bottom:.3rem">' + escHtml(it.question || '—') + '</div>'
|
||
+ '<div style="font-size:.95rem;color:#2a2a2a;line-height:1.45">' + escHtml(it.answer || '—') + '</div>'
|
||
+ '</div>';
|
||
});
|
||
html += '</div>';
|
||
if (items.length >= 50) {
|
||
html += '<p style="font-size:.7rem;color:#8a8a8a;margin-top:.5rem">Letzte 50 Reflexionen — älterere sind im Lehrer-Archiv.</p>';
|
||
}
|
||
host.innerHTML = html;
|
||
} catch (e) {
|
||
var host = document.getElementById('student-reflections-list');
|
||
if (host) host.innerHTML = '<p style="color:#c07a6b;font-size:.82rem">Reflexionen konnten nicht geladen werden.</p>';
|
||
}
|
||
}
|
||
|
||
function renderTeacherProfile(p) {
|
||
var html = '';
|
||
var current = p.emoji_avatar || '👩🏫';
|
||
html += '<div class="card"><div class="avatar-section">';
|
||
html += '<div class="avatar-current" id="avatar-btn" onclick="togglePicker()">' + renderAvatar(current, '👩🏫') + '</div>';
|
||
html += '<div class="avatar-name">' + (p.display_name || '') + '</div>';
|
||
html += '<div class="avatar-role">Lehrperson' + (p.school_name ? ' · ' + p.school_name : '') + '</div>';
|
||
html += '<div class="avatar-picker" id="avatar-picker">';
|
||
AVATAR_GROUPS.forEach(function(g) {
|
||
html += '<div class="avatar-picker-section" style="color:' + (g.color||'#8a8a8a') + '">' + g.name + '</div>';
|
||
(g.items || []).forEach(function(it) {
|
||
var slug = it.slug, name = it.name || slug;
|
||
var val = avatarValue(slug);
|
||
var cls = val === current ? ' active' : '';
|
||
var fb = makeAvatarFallback(slug, g.color || '#4a7c8a');
|
||
html += '<div class="avatar-opt' + cls + '" data-val="' + val + '" title="' + escAttr(name) + '" onclick="selectAvatar(\'' + val + '\')">'
|
||
+ '<img src="' + avatarUrl(slug) + '" alt="' + escAttr(name) + '" onerror="this.onerror=null;this.src=\'' + fb + '\'">'
|
||
+ '<div class="avatar-opt-name">' + escHtml(name) + '</div>'
|
||
+ '</div>';
|
||
});
|
||
});
|
||
html += '<div class="avatar-picker-section">Klassische Symbole</div>';
|
||
EMOJI_OPTIONS.forEach(function(e) {
|
||
var cls = e === current ? ' active' : '';
|
||
html += '<div class="avatar-opt' + cls + '" data-val="' + e + '" onclick="selectAvatar(\'' + e + '\')">' + e + '</div>';
|
||
});
|
||
html += '</div></div></div>';
|
||
|
||
html += '<div class="card"><div class="card-h">Kontoinformationen</div>';
|
||
html += '<div class="info-row"><span class="info-label">E-Mail</span><span class="info-val">' + (p.email || '—') + '</span></div>';
|
||
html += '<div class="info-row"><span class="info-label">Schule</span><span class="info-val">' + (p.school_name || '—') + '</span></div>';
|
||
html += '<div class="info-row"><span class="info-label">Dabei seit</span><span class="info-val">' + new Date(p.created_at).toLocaleDateString('de-AT') + '</span></div>';
|
||
html += '</div>';
|
||
|
||
html += '<div class="card"><div class="card-h">Profil bearbeiten</div>';
|
||
html += '<div class="field"><label>Anzeigename</label><input type="text" id="pf-name" value="' + (p.display_name || '') + '"></div>';
|
||
html += '<div class="field"><label>E-Mail</label><input type="email" id="pf-email" value="' + (p.email || '') + '"></div>';
|
||
html += '<div class="field"><label>Schule</label><input type="text" id="pf-school" value="' + (p.school_name || '') + '"></div>';
|
||
html += '<div class="field"><label>Land (bestimmt die „lokale" Herkunft der Zutaten in Weltküche)</label><select id="pf-country">' +
|
||
[['AT','🇦🇹 Österreich'],['DE','🇩🇪 Deutschland'],['CH','🇨🇭 Schweiz'],['LI','🇱🇮 Liechtenstein']]
|
||
.map(function(o){ return '<option value="'+o[0]+'"'+(((p.country||'AT').toUpperCase())===o[0]?' selected':'')+'>'+o[1]+'</option>'; }).join('') +
|
||
'</select></div>';
|
||
html += '<button class="btn" onclick="saveTeacher()">Speichern</button>';
|
||
html += '<div class="msg" id="msg-teacher"></div>';
|
||
html += '</div>';
|
||
|
||
html += '<div class="card"><div class="card-h">Passwort ändern</div>';
|
||
html += '<div class="field"><label>Altes Passwort</label><input type="password" id="pf-oldpw"></div>';
|
||
html += '<div class="field"><label>Neues Passwort (mind. 8 Zeichen)</label><input type="password" id="pf-newpw"></div>';
|
||
html += '<button class="btn" onclick="changePw()">Passwort ändern</button>';
|
||
html += '<div class="msg" id="msg-pw"></div>';
|
||
html += '</div>';
|
||
|
||
html += '<div class="card">';
|
||
html += '<a href="teacher.html" class="btn" style="text-decoration:none;display:inline-block">Zum Dashboard</a>';
|
||
html += '</div>';
|
||
|
||
html += '<div class="card" style="border-color:#f2ddd8">';
|
||
html += '<div class="card-h" style="color:#c07a6b">Konto löschen</div>';
|
||
html += '<p style="font-size:.7rem;color:#8a8a8a;margin-bottom:.5rem">Ihr Konto wird deaktiviert. Ihre Klassen bleiben erhalten, Schüler*innen werden zu Autodidakten.</p>';
|
||
html += '<button class="btn" style="background:#c07a6b" onclick="deleteAccount()">Konto endgültig löschen</button>';
|
||
html += '</div>';
|
||
|
||
document.getElementById('profile-content').innerHTML = html;
|
||
}
|
||
|
||
function togglePicker() {
|
||
document.getElementById('avatar-picker').classList.toggle('open');
|
||
}
|
||
|
||
async function selectAvatar(value) {
|
||
// value kann 'avatar:<slug>' ODER ein Emoji-Zeichen sein.
|
||
// textContent würde 'avatar:eule-prof' wörtlich als Text rendern → riesige
|
||
// Buchstaben im Kreis. Stattdessen renderAvatar → IMG oder Emoji-Text.
|
||
document.getElementById('avatar-btn').innerHTML = renderAvatar(value);
|
||
document.querySelectorAll('.avatar-opt').forEach(function(o) {
|
||
o.classList.toggle('active', o.dataset.val === value);
|
||
});
|
||
// User-Pille oben rechts SOFORT aktualisieren — keine Reload-Pflicht.
|
||
// Beide Selektoren probieren, weil schueler.html '#user-pill .emoji' nutzt
|
||
// und profil.html ein '#user-emoji' direkt vergibt. Cache-Buster via ?v=
|
||
// erzwingt Re-Render, falls der Browser das Pillen-Bild gecached hat.
|
||
var pillEmojis = [
|
||
document.getElementById('user-emoji'),
|
||
document.querySelector('#user-pill .emoji'),
|
||
document.querySelector('.user-pill .emoji'),
|
||
].filter(function(el, i, arr) { return el && arr.indexOf(el) === i; });
|
||
var slug = avatarSlugFromValue(value);
|
||
var cacheBuster = '?v=' + Date.now();
|
||
pillEmojis.forEach(function(pillEmoji) {
|
||
if (slug) {
|
||
var fb = makeAvatarFallback(slug, avatarColorForSlug(slug));
|
||
pillEmoji.innerHTML = '<img src="' + avatarThumbUrl(slug) + cacheBuster + '" alt="" onerror="this.onerror=null;this.src=\'' + fb + '\'">';
|
||
} else {
|
||
pillEmoji.innerHTML = '';
|
||
pillEmoji.textContent = value;
|
||
}
|
||
});
|
||
await api('profile', {emojiAvatar: value});
|
||
}
|
||
|
||
async function saveName() {
|
||
var name = document.getElementById('pf-name').value.trim();
|
||
if (!name) return;
|
||
var r = await api('profile', {displayName: name});
|
||
var m = document.getElementById('msg-name');
|
||
m.className = r.error ? 'msg err' : 'msg ok';
|
||
m.textContent = r.error || 'Gespeichert!';
|
||
}
|
||
|
||
async function saveTeacher() {
|
||
var name = document.getElementById('pf-name').value.trim();
|
||
var email = document.getElementById('pf-email').value.trim();
|
||
var school = document.getElementById('pf-school').value.trim();
|
||
var country = (document.getElementById('pf-country') || {}).value || 'AT';
|
||
var r = await api('profile', {displayName: name, email: email, schoolName: school, country: country});
|
||
var m = document.getElementById('msg-teacher');
|
||
m.className = r.error ? 'msg err' : 'msg ok';
|
||
m.textContent = r.error || 'Gespeichert!';
|
||
}
|
||
|
||
async function changePw() {
|
||
var old = document.getElementById('pf-oldpw').value;
|
||
var pw = document.getElementById('pf-newpw').value;
|
||
if (!old || !pw) return;
|
||
var r = await api('profile', {oldPassword: old, newPassword: pw});
|
||
var m = document.getElementById('msg-pw');
|
||
m.className = r.error ? 'msg err' : 'msg ok';
|
||
m.textContent = r.error || 'Passwort geändert!';
|
||
if (!r.error) { document.getElementById('pf-oldpw').value = ''; document.getElementById('pf-newpw').value = ''; }
|
||
}
|
||
|
||
async function deleteAccount() {
|
||
if (!confirm('Sind Sie sicher? Ihr Konto wird deaktiviert. Diese Aktion kann nicht rückgängig gemacht werden.')) return;
|
||
if (!confirm('Wirklich löschen? Alle Ihre Klassen werden archiviert.')) return;
|
||
var r = await api('profile', {action: 'delete_account'});
|
||
if (r.error) { alert(r.error); return; }
|
||
alert('Konto wurde gelöscht.');
|
||
location.href = 'index.html';
|
||
}
|
||
|
||
loadProfile();
|
||
</script>
|
||
<script src="assets/js/version-tag.js" defer></script>
|
||
</body>
|
||
</html>
|