Files
Adminator 1e51ef7def Nachtrag: alle bisher untracked Ordner + hängende Änderungen mit-committen
- Konzept/, didaktik_geografie/, didaktik_simulation/, v2-modules/, v2-platform/
- 12 code-workspace-Files
- STATUS-*.md
- viele M/D/R-Änderungen an bereits getrackten Files
- .gitignore verstärkt: **/.humaninput/, **/secret_keys.txt

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-08 02:27:02 +02:00

287 lines
9.2 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>GeoGraSim V2 — Cockpit</title>
<link rel="stylesheet" href="./assets/css/design-system-v2.css">
<style>
body {
background: var(--ggs-bg);
min-height: 100dvh;
padding-bottom: env(safe-area-inset-bottom);
}
.topbar {
background: var(--ggs-white);
border-bottom: 1px solid var(--ggs-border);
padding: var(--ggs-gap-sm) var(--ggs-gap-md);
display: flex; align-items: center; gap: var(--ggs-gap-md);
position: sticky; top: 0; z-index: 100;
}
.topbar h1 {
font-size: 1rem; color: var(--ggs-fjord-dark); font-weight: 800;
flex: 1;
}
.student-pill {
display: flex; align-items: center; gap: var(--ggs-gap-sm);
padding: 4px var(--ggs-gap-md);
background: var(--ggs-bg);
border-radius: var(--ggs-radius-pill);
}
.student-pill img { width: 28px; height: 28px; border-radius: 50%; }
.student-pill .name { font-size: 0.85rem; font-weight: 600; }
.student-pill .class { font-size: 0.72rem; color: var(--ggs-text-muted); }
main {
max-width: 1100px; margin: 0 auto;
padding: var(--ggs-gap-lg) var(--ggs-gap-md);
}
section { margin-bottom: var(--ggs-gap-xl); }
section h2 {
font-size: 0.95rem; font-weight: 800; color: var(--ggs-fjord-dark);
margin-bottom: var(--ggs-gap-md);
display: flex; align-items: center; gap: var(--ggs-gap-sm);
}
section h2 .badge {
background: var(--ggs-coral); color: var(--ggs-white);
padding: 2px 8px; border-radius: var(--ggs-radius-pill);
font-size: 0.7rem;
}
.module-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: var(--ggs-gap-md);
}
.module-card {
background: var(--ggs-white);
border: 2px solid var(--ggs-border);
border-radius: var(--ggs-radius-md);
overflow: hidden;
cursor: pointer;
transition: border-color var(--ggs-anim-fast), transform var(--ggs-anim-fast);
display: flex; flex-direction: column;
}
.module-card[data-assignment="1"] {
border-color: var(--ggs-coral);
box-shadow: 0 0 0 2px var(--ggs-coral-light);
}
@media (hover: hover) {
.module-card:hover { border-color: var(--ggs-fjord); transform: translateY(-2px); }
}
.module-card .card-img {
width: 100%; height: 130px; object-fit: cover;
background: var(--ggs-bg-dark);
}
.module-card .card-icon-fallback {
width: 100%; height: 130px;
display: flex; align-items: center; justify-content: center;
background: var(--ggs-fjord-light); font-size: 3rem;
}
.module-card .card-body {
padding: var(--ggs-gap-md);
display: flex; flex-direction: column; gap: var(--ggs-gap-xs);
flex: 1;
}
.module-card h3 {
font-size: 0.95rem; color: var(--ggs-fjord-dark);
}
.module-card .desc {
font-size: 0.78rem; color: var(--ggs-text-muted);
flex: 1;
}
.module-card .meta {
display: flex; align-items: center; gap: var(--ggs-gap-sm);
font-size: 0.72rem; color: var(--ggs-text-muted);
margin-top: var(--ggs-gap-sm);
}
.module-card .meta .pill-assignment {
background: var(--ggs-coral); color: var(--ggs-white);
padding: 2px 8px; border-radius: var(--ggs-radius-pill);
font-weight: 700;
}
.module-card .meta .pill-level {
background: var(--ggs-fjord-light); color: var(--ggs-fjord-dark);
padding: 2px 8px; border-radius: var(--ggs-radius-pill);
}
.empty-hint {
background: var(--ggs-white);
border: 1px dashed var(--ggs-border);
border-radius: var(--ggs-radius-md);
padding: var(--ggs-gap-lg);
text-align: center;
color: var(--ggs-text-muted);
font-size: 0.85rem;
}
.loading { padding: var(--ggs-gap-xl); text-align: center; color: var(--ggs-text-muted); }
</style>
</head>
<body>
<div class="topbar">
<h1>GeoGraSim V2 · Schüler*in-Cockpit</h1>
<div class="student-pill" id="studentPill" hidden>
<img id="avatar" src="" alt="">
<div>
<div class="name" id="displayName"></div>
<div class="class" id="className"></div>
</div>
</div>
<button class="ggs-btn ggs-btn-secondary" id="logoutBtn" type="button">Abmelden</button>
</div>
<main>
<div class="loading" id="loading">Wird geladen …</div>
<section id="assignmentsSection" hidden>
<h2>📌 Aktuelle Aufträge <span class="badge" id="assignmentsCount">0</span></h2>
<div class="module-grid" id="assignmentsGrid"></div>
</section>
<section id="freeSection" hidden>
<h2>🎯 Freies Arbeiten</h2>
<div class="module-grid" id="freeGrid"></div>
</section>
<section id="emptySection" hidden>
<div class="empty-hint">
Aktuell sind keine Module für deine Klasse freigegeben. Wende dich an deine Lehrperson.
</div>
</section>
</main>
<script type="module">
import { auth } from './assets/js/auth-client.js';
if (!auth.token || auth.role !== 'student') {
location.replace('./login.html');
}
document.getElementById('logoutBtn').addEventListener('click', async () => {
await auth.logout();
location.replace('./login.html');
});
const $ = id => document.getElementById(id);
const easy = !!auth.user; // wir setzen unten
async function load() {
await auth.maybeRefresh();
const [me, modules] = await Promise.all([
auth.api('GET', 'student/me.php'),
auth.api('GET', 'modules/available.php'),
]);
// Topbar
$('avatar').src = me.avatarUrl;
$('avatar').alt = me.displayName;
$('displayName').textContent = me.displayName;
$('className').textContent = me.className;
$('studentPill').hidden = false;
// A11y-Defaults aus Profil auf <body> spiegeln
const p = me.preferences || {};
if (p.highContrast) document.body.dataset.highContrast = 'light';
if (p.reducedMotion) document.body.dataset.reducedMotion = 'true';
if (p.textScale && p.textScale !== 'normal') document.body.dataset.textScale = p.textScale;
$('loading').hidden = true;
const useEasy = me.easyLanguage;
// Aufträge
if (modules.assignments?.length) {
$('assignmentsSection').hidden = false;
$('assignmentsCount').textContent = modules.assignments.length;
$('assignmentsGrid').innerHTML = modules.assignments
.map(m => renderCard(m, { assignment: true, useEasy, nachteilsausgleich: me.nachteilsausgleich }))
.join('');
}
// Freie Module
if (modules.freeModules?.length) {
$('freeSection').hidden = false;
$('freeGrid').innerHTML = modules.freeModules
.map(m => renderCard(m, { assignment: false, useEasy, nachteilsausgleich: me.nachteilsausgleich }))
.join('');
}
if (!modules.assignments?.length && !modules.freeModules?.length) {
$('emptySection').hidden = false;
}
// Click-Handler
document.querySelectorAll('.module-card').forEach(c => {
c.addEventListener('click', () => startModule(c.dataset, me));
});
}
function renderCard(m, opts) {
const name = opts.useEasy ? (m.displayNameEasy || m.displayName) : m.displayName;
const desc = opts.useEasy ? (m.shortDescriptionEasy || m.shortDescription) : m.shortDescription;
const img = m.cardImageUrl
? `<img class="card-img" src="${escapeAttr(m.cardImageUrl)}" alt="">`
: `<div class="card-icon-fallback" aria-hidden="true">${escapeHtml(m.icon || '📘')}</div>`;
const pills = [];
if (opts.assignment) {
pills.push(`<span class="pill-assignment">Auftrag · ${escapeHtml(m.difficulty)}</span>`);
} else if (m.unlocked) {
const hu = m.unlocked.highestUnlocked || 'L1';
pills.push(`<span class="pill-level">bis ${escapeHtml(hu)} frei</span>`);
}
if (m.estimatedDurationMin) {
pills.push(`<span>~ ${m.estimatedDurationMin} Min</span>`);
}
return `
<article class="module-card"
data-assignment="${opts.assignment ? '1' : '0'}"
data-slug="${escapeAttr(m.moduleSlug || m.slug)}"
data-difficulty="${escapeAttr(m.difficulty || (m.unlocked?.highestUnlocked || 'L1'))}"
data-mode="${opts.assignment ? 'teacher_started' : 'free'}"
tabindex="0" role="button"
aria-label="${escapeAttr(name)}">
${img}
<div class="card-body">
<h3>${escapeHtml(name)}</h3>
<p class="desc">${escapeHtml(desc || '')}</p>
<div class="meta">${pills.join('')}</div>
</div>
</article>`;
}
function startModule(data, me) {
// URL zum Modul bauen: v2-modules/<slug>/public/game.html
// ?session_token=...&student_id=...&class_id=...&mode=...&difficulty=...&lang=...&mock=...
const params = new URLSearchParams({
session_token: auth.token,
student_id: me.studentId,
class_id: me.classId,
mode: data.mode,
difficulty: data.difficulty,
lang: me.locale,
resume: '0',
});
// Beim Mock-Cockpit hängen wir mock=1 an, damit das Modul gegen die Mock geht
if (location.pathname.includes('/mock/')) params.set('mock', '1');
// Pfad zum Modul — vom v2-platform-Wurzel zwei Ebenen hoch + v2-modules/<slug>/public/game.html
const moduleUrl = `../v2-modules/${data.slug}/public/game.html?${params}`;
location.href = moduleUrl;
}
function escapeHtml(s) {
return String(s ?? '').replace(/[&<>"']/g, c => ({
'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;',
}[c]));
}
function escapeAttr(s) { return escapeHtml(s); }
load().catch(err => {
$('loading').textContent = 'Fehler beim Laden: ' + err.message;
});
</script>
</body>
</html>