2bdaa0f6cc
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
372 lines
18 KiB
PHP
372 lines
18 KiB
PHP
<?php
|
|
/* Login-Page mit Hero-Background-Overlay (Atlas-Empfehlung Option B, L.38).
|
|
Standard-Hauptnav klar oben, dahinter atmosphärischer Landingpage-Hero
|
|
geblurred, Login-Card als zentriertes Modal. Klick auf den Backdrop →
|
|
Landingpage; ESC ebenfalls. */
|
|
require_once __DIR__ . '/php/config/app.php';
|
|
$bp = BASE_PATH;
|
|
$activeMain = null;
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>GeoGraSim — Anmelden</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}
|
|
|
|
/* Login-Shell — füllt den Bereich unter der Hauptnav */
|
|
.login-shell { position: relative; min-height: calc(100vh - 56px); overflow: hidden; }
|
|
.login-shell .hero-bg { z-index: 0; }
|
|
.login-overlay {
|
|
position: absolute; inset: 0; z-index: 1;
|
|
backdrop-filter: blur(2px);
|
|
-webkit-backdrop-filter: blur(2px);
|
|
background: rgba(247, 246, 243, 0.10);
|
|
display: flex; align-items: center; justify-content: center;
|
|
padding: 2rem 1rem;
|
|
cursor: pointer;
|
|
}
|
|
.login-card-inner { cursor: default; }
|
|
|
|
/* Login-Card — 2-spaltig (Anmelden links · Registrieren rechts) */
|
|
.card{background:#fff;border-radius:16px;padding:1.6rem 1.8rem;max-width:760px;width:96%;box-shadow:0 20px 60px rgba(0,0,0,.18)}
|
|
.login-grid{display:grid;grid-template-columns:1fr 1fr;gap:1.8rem;align-items:start;margin-top:.4rem}
|
|
.lg-col{display:flex;flex-direction:column}
|
|
.lg-col-register{border-left:1px solid rgba(0,0,0,.08);padding-left:1.8rem}
|
|
.lg-col h2{font-size:.95rem;font-weight:800;color:#1f4e5a;margin-bottom:.6rem;text-align:center}
|
|
@media (max-width:700px){
|
|
.card{padding:1.4rem 1.3rem;max-width:420px}
|
|
.login-grid{grid-template-columns:1fr;gap:1rem}
|
|
.lg-col-register{border-left:none;padding-left:0;border-top:1px solid rgba(0,0,0,.08);padding-top:1rem}
|
|
}
|
|
.logo{text-align:center;margin-bottom:1rem}
|
|
.logo p{font-size:.78rem;color:#8a8a8a;margin-top:.3rem}
|
|
label{display:block;font-size:.72rem;font-weight:600;color:#4a4a4a;margin-bottom:.2rem;margin-top:.8rem}
|
|
input{width:100%;padding:.55rem .7rem;border:1.5px solid rgba(0,0,0,.1);border-radius:7px;font-size:.82rem;font-family:inherit;outline:none;transition:border .15s}
|
|
input:focus{border-color:#4a7c8a}
|
|
.btn{width:100%;padding:.6rem;border:none;border-radius:8px;background:#4a7c8a;color:#fff;font-size:.85rem;font-weight:700;cursor:pointer;margin-top:1rem;transition:background .15s}
|
|
.btn:hover{background:#3a6470}
|
|
.btn-secondary{background:transparent;color:#4a7c8a;border:1.5px solid #4a7c8a}
|
|
.btn-secondary:hover{background:#dae8ec}
|
|
.msg{margin-top:.8rem;font-size:.75rem;padding:.4rem .6rem;border-radius:6px;display:none}
|
|
.msg.err{display:block;background:#f2ddd8;color:#c07a6b}
|
|
.msg.ok{display:block;background:#dceadd;color:#5a8a5e}
|
|
.link{text-align:center;margin-top:.8rem;font-size:.72rem;color:#8a8a8a}
|
|
.link a{color:#4a7c8a;text-decoration:none;font-weight:600}
|
|
.hidden{display:none !important}
|
|
.pw-wrap{position:relative}
|
|
.pw-wrap input{padding-right:2.2rem}
|
|
.pw-eye{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);cursor:pointer;font-size:1rem;opacity:.4;user-select:none;line-height:1}
|
|
.pw-eye:hover{opacity:.7}
|
|
.tabs{display:flex;gap:0;margin-bottom:1rem;border-radius:8px;overflow:hidden;border:1px solid rgba(0,0,0,.08)}
|
|
.tab{flex:1;padding:.5rem;text-align:center;font-size:.75rem;font-weight:600;cursor:pointer;background:#f7f6f3;color:#8a8a8a;transition:all .15s}
|
|
.tab.on{background:#4a7c8a;color:#fff}
|
|
.divider{text-align:center;margin:1rem 0;color:#ccc;font-size:.72rem;position:relative}
|
|
.divider::before,.divider::after{content:'';position:absolute;top:50%;width:35%;height:1px;background:#e0e0e0}
|
|
.divider::before{left:0}
|
|
.divider::after{right:0}
|
|
.quick-section{border-top:1px solid rgba(0,0,0,.06);margin-top:1.2rem;padding-top:1rem}
|
|
.quick-section p{font-size:.68rem;color:#8a8a8a;text-align:center;margin-bottom:.6rem}
|
|
.quick-btns{display:flex;gap:.35rem}
|
|
.quick-btns button{flex:1;font-size:.72rem;padding:.45rem}
|
|
.dsgvo-row{
|
|
display:flex;align-items:flex-start;gap:.45rem;margin-top:1rem;
|
|
font-size:.72rem;font-weight:500;color:#4a4a4a;cursor:pointer
|
|
}
|
|
.dsgvo-row input{width:auto;margin-top:.18rem;cursor:pointer}
|
|
.dsgvo-row a{color:#4a7c8a;font-weight:600;text-decoration:underline}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<?php include __DIR__ . '/pages/_partials/public_topnav.php'; ?>
|
|
|
|
<div class="login-shell">
|
|
<?php include __DIR__ . '/pages/_partials/public_hero_bg.php'; ?>
|
|
|
|
<div class="login-overlay" id="login-overlay">
|
|
<div class="login-card-inner">
|
|
<div class="card">
|
|
<div class="logo">
|
|
<a href="index.html"><img src="assets/img/geograsim_t.svg" alt="GeoGraSim" style="height:52px"></a>
|
|
<p>Interaktive Simulationen für den Geografieunterricht</p>
|
|
</div>
|
|
|
|
<div class="login-grid">
|
|
|
|
<!-- ============== SPALTE LINKS: ANMELDEN ↔ REGISTRIEREN (Toggle) + Forgot/Reset ============== -->
|
|
<div class="lg-col lg-col-login">
|
|
|
|
<div id="view-login">
|
|
<h2>Einloggen</h2>
|
|
<label>E-Mail oder Benutzername</label>
|
|
<input type="text" id="l-user" placeholder="name@schule.at oder schueler01">
|
|
<p style="font-size:.66rem;color:#8a8a8a;margin:.15rem 0 .3rem">Lehrpersonen melden sich mit ihrer <strong>E-Mail-Adresse</strong> an, Schüler*innen mit ihrem <strong>Benutzernamen</strong>.</p>
|
|
<label>Passwort</label>
|
|
<div class="pw-wrap"><input type="password" id="l-pass"><span class="pw-eye" onclick="togglePw(this)">👁</span></div>
|
|
<button class="btn" onclick="doLogin()">Einloggen</button>
|
|
<div class="link"><a href="#" onclick="showView('forgot');return false">Passwort vergessen?</a></div>
|
|
<div class="divider">oder</div>
|
|
<button class="btn btn-secondary" onclick="showView('register')" style="margin-top:0">Registrieren — Account erstellen</button>
|
|
</div>
|
|
|
|
<div id="view-register" class="hidden">
|
|
<h2>Registrieren</h2>
|
|
<div class="tabs">
|
|
<div class="tab on" onclick="setRegType('teacher')">Lehrperson</div>
|
|
<div class="tab" onclick="setRegType('student')">Schüler*in</div>
|
|
</div>
|
|
|
|
<div id="reg-teacher">
|
|
<label>E-Mail-Adresse</label>
|
|
<input type="email" id="r-email" placeholder="name@schule.at">
|
|
<p style="font-size:.66rem;color:#8a8a8a;margin:.15rem 0 .4rem">Ihre E-Mail-Adresse ist zugleich Ihr <strong>Benutzername</strong> für die Anmeldung.</p>
|
|
<label>Passwort (mind. 8 Zeichen)</label>
|
|
<div class="pw-wrap"><input type="password" id="r-pass" minlength="8"><span class="pw-eye" onclick="togglePw(this)">👁</span></div>
|
|
<label>Passwort bestätigen</label>
|
|
<input type="password" id="r-pass2">
|
|
<label class="dsgvo-row">
|
|
<input type="checkbox" id="r-dsgvo">
|
|
<span>Ich akzeptiere die <a href="datenschutz.php" target="_blank" rel="noopener">Datenschutzerklärung</a>.</span>
|
|
</label>
|
|
<button class="btn" onclick="doRegisterTeacher()">Registrieren</button>
|
|
</div>
|
|
|
|
<div id="reg-student" class="hidden">
|
|
<label>Klassencode</label>
|
|
<input type="text" id="rs-code" placeholder="z.B. GEO1A" maxlength="6" autocapitalize="characters">
|
|
<label>Benutzername</label>
|
|
<input type="text" id="rs-user" placeholder="z.B. max01">
|
|
<label>Passwort (mind. 4 Zeichen)</label>
|
|
<div class="pw-wrap"><input type="password" id="rs-pass"><span class="pw-eye" onclick="togglePw(this)">👁</span></div>
|
|
<p style="font-size:.68rem;color:#8a8a8a;margin-top:.5rem">Schüler*innen-Zugänge werden von der Lehrperson erstellt. Frag nach Klassencode + Zugangsdaten.</p>
|
|
<button class="btn" onclick="doRegisterStudent()">Registrieren</button>
|
|
</div>
|
|
|
|
<div class="link"><a href="#" onclick="showView('login');return false">← Bereits registriert? Anmelden</a></div>
|
|
</div>
|
|
|
|
<!-- PASSWORT VERGESSEN -->
|
|
<div id="view-forgot" class="hidden">
|
|
<h2>Passwort zurücksetzen</h2>
|
|
<label>E-Mail-Adresse</label>
|
|
<input type="email" id="f-email" placeholder="name@schule.at">
|
|
<button class="btn" onclick="doResetRequest()">Link senden</button>
|
|
<div class="link"><a href="#" onclick="showView('login');return false">← Zurück zur Anmeldung</a></div>
|
|
</div>
|
|
|
|
<!-- PASSWORT RESET (via URL-Token) -->
|
|
<div id="view-reset" class="hidden">
|
|
<h2>Neues Passwort setzen</h2>
|
|
<label>Neues Passwort (mind. 8 Zeichen)</label>
|
|
<div class="pw-wrap"><input type="password" id="n-pass" minlength="8"><span class="pw-eye" onclick="togglePw(this)">👁</span></div>
|
|
<label>Passwort bestätigen</label>
|
|
<input type="password" id="n-pass2">
|
|
<button class="btn" onclick="doResetConfirm()">Passwort setzen</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============== SPALTE RECHTS: SCHNELL-LOGIN (temporäre Test-Sektion, später raus) ============== -->
|
|
<div class="lg-col lg-col-register">
|
|
<h2>Zum Ausprobieren</h2>
|
|
<p style="font-size:.7rem;color:#8a8a8a;margin-bottom:.7rem;text-align:center">Schnell-Login mit Demo-Accounts:</p>
|
|
<div style="display:flex;flex-direction:column;gap:.4rem">
|
|
<button class="btn-secondary btn" onclick="quickLogin('teacher')" style="width:100%;font-size:.78rem">👩🏫 Test-Lehrperson</button>
|
|
<div class="quick-btns" style="display:flex;gap:.35rem">
|
|
<button class="btn-secondary btn" onclick="quickLogin('anna')" style="flex:1;font-size:.72rem;padding:.45rem">👧 Anna</button>
|
|
<button class="btn-secondary btn" onclick="quickLogin('ben')" style="flex:1;font-size:.72rem;padding:.45rem">👦 Ben</button>
|
|
<button class="btn-secondary btn" onclick="quickLogin('clara')" style="flex:1;font-size:.72rem;padding:.45rem">👧 Clara</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="msg" id="msg"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Klick außerhalb der Card auf den Overlay → zurück zur Landingpage
|
|
document.getElementById('login-overlay').addEventListener('click', function(e) {
|
|
if (e.target.closest('.login-card-inner')) return;
|
|
location.href = 'index.html';
|
|
});
|
|
// ESC schließt ebenfalls
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') location.href = 'index.html';
|
|
});
|
|
|
|
function togglePw(el) {
|
|
var inp = el.previousElementSibling;
|
|
if (inp.type === 'password') { inp.type = 'text'; el.textContent = '🙈'; }
|
|
else { inp.type = 'password'; el.textContent = '👁'; }
|
|
}
|
|
|
|
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'; } }
|
|
|
|
function showMsg(text, type) {
|
|
var m = document.getElementById('msg');
|
|
m.textContent = text; m.className = 'msg ' + type;
|
|
}
|
|
|
|
async function api(action, data) {
|
|
var res = await fetch(BASE + '/api/auth', {
|
|
method: 'POST', headers: {'Content-Type':'application/json'}, credentials:'same-origin',
|
|
body: JSON.stringify(Object.assign({action: action}, data))
|
|
});
|
|
return await res.json();
|
|
}
|
|
|
|
// === VIEWS ===
|
|
// Linke Spalte = Toggle login ↔ register ↔ forgot ↔ reset.
|
|
// Rechte Spalte = permanenter Schnell-Login-Block (außerhalb des Toggles).
|
|
function showView(view) {
|
|
['login','register','forgot','reset'].forEach(function(v) {
|
|
document.getElementById('view-' + v).classList.toggle('hidden', v !== view);
|
|
});
|
|
document.getElementById('msg').className = 'msg';
|
|
}
|
|
|
|
function setRegType(type) {
|
|
document.querySelectorAll('.tabs .tab').forEach(function(t, i) {
|
|
t.classList.toggle('on', (i === 0 && type === 'teacher') || (i === 1 && type === 'student'));
|
|
});
|
|
document.getElementById('reg-teacher').classList.toggle('hidden', type !== 'teacher');
|
|
document.getElementById('reg-student').classList.toggle('hidden', type !== 'student');
|
|
}
|
|
|
|
// === LOGIN (einheitlich) ===
|
|
async function doLogin() {
|
|
var user = document.getElementById('l-user').value.trim();
|
|
var pass = document.getElementById('l-pass').value;
|
|
if (!user || !pass) { showMsg('Bitte ausfüllen.', 'err'); return; }
|
|
|
|
var r = await api('login', {email: user, password: pass});
|
|
if (!r.error) {
|
|
showMsg('Willkommen, ' + r.displayName + '!', 'ok');
|
|
setTimeout(function() { location.href = 'teacher.html'; }, 600);
|
|
return;
|
|
}
|
|
|
|
var r2 = await api('student_login_by_username', {username: user, password: pass});
|
|
if (!r2.error) {
|
|
showMsg('Willkommen, ' + r2.displayName + '!', 'ok');
|
|
setTimeout(function() { location.href = 'schueler.html'; }, 600);
|
|
return;
|
|
}
|
|
|
|
showMsg('Benutzername oder Passwort falsch.', 'err');
|
|
}
|
|
|
|
// === REGISTRIERUNG LEHRPERSON ===
|
|
// Minimal-Felder: nur E-Mail + Passwort + DSGVO-Akzeptanz. displayName wird
|
|
// aus dem E-Mail-Local-Part abgeleitet (Backend braucht ihn als nicht-leer);
|
|
// echter Anzeigename lässt sich später im Backend/Profil ergänzen.
|
|
async function doRegisterTeacher() {
|
|
var email = document.getElementById('r-email').value.trim();
|
|
var pass = document.getElementById('r-pass').value;
|
|
var pass2 = document.getElementById('r-pass2').value;
|
|
var dsgvo = document.getElementById('r-dsgvo').checked;
|
|
if (!email || !pass) { showMsg('Bitte E-Mail und Passwort ausfüllen.', 'err'); return; }
|
|
if (pass !== pass2) { showMsg('Passwörter stimmen nicht überein.', 'err'); return; }
|
|
if (pass.length < 8) { showMsg('Passwort muss mindestens 8 Zeichen haben.', 'err'); return; }
|
|
if (!dsgvo) { showMsg('Bitte Datenschutzerklärung akzeptieren.', 'err'); return; }
|
|
var displayName = (email.split('@')[0] || 'Lehrperson').substring(0, 64);
|
|
var r = await api('register', {displayName: displayName, schoolName: '', email: email, password: pass});
|
|
if (r.error) { showMsg(r.error, 'err'); return; }
|
|
showMsg('Registrierung erfolgreich! Klasse "Meine Klasse" wurde angelegt.', 'ok');
|
|
setTimeout(function() { location.href = 'teacher.html'; }, 1200);
|
|
}
|
|
|
|
// === REGISTRIERUNG SCHÜLER*IN ===
|
|
async function doRegisterStudent() {
|
|
var code = document.getElementById('rs-code').value.trim().toUpperCase();
|
|
var user = document.getElementById('rs-user').value.trim();
|
|
var pass = document.getElementById('rs-pass').value;
|
|
if (!code || !user || !pass) { showMsg('Alle Felder ausfüllen.', 'err'); return; }
|
|
if (pass.length < 4) { showMsg('Passwort muss mindestens 4 Zeichen haben.', 'err'); return; }
|
|
var r = await api('student_register', {classCode: code, username: user, password: pass});
|
|
if (r.error) { showMsg(r.error, 'err'); return; }
|
|
showMsg('Account erstellt! Willkommen, ' + r.displayName + '!', 'ok');
|
|
setTimeout(function() { location.href = 'schueler.html'; }, 800);
|
|
}
|
|
|
|
// === PASSWORT VERGESSEN ===
|
|
async function doResetRequest() {
|
|
var email = document.getElementById('f-email').value.trim();
|
|
if (!email) { showMsg('Bitte E-Mail-Adresse eingeben.', 'err'); return; }
|
|
var r = await api('reset_request', {email: email});
|
|
showMsg(r.message || 'Falls ein Konto existiert, wurde eine E-Mail gesendet.', 'ok');
|
|
}
|
|
|
|
// === PASSWORT RESET (Token) ===
|
|
var resetToken = new URLSearchParams(location.search).get('reset');
|
|
if (resetToken) { showView('reset'); }
|
|
|
|
// === QR-Code-Deeplink: ?user=... &class=... vorbelegen ===
|
|
// Schüler*innen scannen ihren Klassen-Ticket-QR → User ist automatisch im Feld.
|
|
(function(){
|
|
var p = new URLSearchParams(location.search);
|
|
var u = p.get('user'); var c = p.get('class');
|
|
if (u) {
|
|
var el = document.getElementById('l-user'); if (el) el.value = u;
|
|
// Klassencode könnten wir in den Username schreiben — Login akzeptiert nur User+PIN,
|
|
// der Klassencode ist NICHT teil des Schüler-Login-Flows (er ist nur fürs Register).
|
|
// PIN-Feld kriegt den Fokus, damit Schüler*in nur noch PIN tippen muss.
|
|
var pw = document.getElementById('l-pass'); if (pw) pw.focus();
|
|
}
|
|
})();
|
|
|
|
async function doResetConfirm() {
|
|
var pass = document.getElementById('n-pass').value;
|
|
var pass2 = document.getElementById('n-pass2').value;
|
|
if (!pass || pass.length < 8) { showMsg('Passwort muss mindestens 8 Zeichen haben.', 'err'); return; }
|
|
if (pass !== pass2) { showMsg('Passwörter stimmen nicht überein.', 'err'); return; }
|
|
var r = await api('reset_confirm', {token: resetToken, newPassword: pass});
|
|
if (r.error) { showMsg(r.error, 'err'); return; }
|
|
showMsg('Passwort geändert! Sie können sich jetzt anmelden.', 'ok');
|
|
setTimeout(function() { location.href = 'login.html'; }, 1500);
|
|
}
|
|
|
|
// === QUICK LOGIN ===
|
|
async function quickLogin(who) {
|
|
showMsg('Anmelden...', 'ok');
|
|
if (who === 'teacher') {
|
|
var r = await api('login', {email: 'test@geograsim.at', password: 'test1234'});
|
|
if (r.error) { showMsg(r.error, 'err'); return; }
|
|
showMsg('Willkommen, ' + r.displayName + '!', 'ok');
|
|
setTimeout(function() { location.href = 'teacher.html'; }, 600);
|
|
} else {
|
|
var r = await api('student_login', {classCode: 'TEST01', username: who, password: 'test1234'});
|
|
if (r.error) { showMsg(r.error, 'err'); return; }
|
|
showMsg('Willkommen, ' + r.displayName + '!', 'ok');
|
|
setTimeout(function() { location.href = 'schueler.html'; }, 600);
|
|
}
|
|
}
|
|
|
|
// Enter-Key
|
|
document.getElementById('l-pass').addEventListener('keydown', function(e) { if(e.key==='Enter') doLogin(); });
|
|
document.getElementById('l-user').addEventListener('keydown', function(e) { if(e.key==='Enter') doLogin(); });
|
|
|
|
// Check ob schon eingeloggt
|
|
if (!resetToken) {
|
|
api('status', {}).then(function(r) {
|
|
if (r.role === 'teacher') location.href = 'teacher.html';
|
|
if (r.role === 'student') location.href = 'schueler.html';
|
|
});
|
|
}
|
|
</script>
|
|
<script src="assets/js/version-tag.js" defer></script>
|
|
</body>
|
|
</html>
|