1e51ef7def
- 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>
287 lines
9.2 KiB
HTML
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 => ({
|
|
'&':'&','<':'<','>':'>','"':'"',"'":''',
|
|
}[c]));
|
|
}
|
|
function escapeAttr(s) { return escapeHtml(s); }
|
|
|
|
load().catch(err => {
|
|
$('loading').textContent = 'Fehler beim Laden: ' + err.message;
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|