Files
geograsim/App/admin-modules.html
T
Adminator 9a61f55cb1 Atlas: Infrastruktur + Team-Konventionen + Sprachregel Lernarbeit
- Design-System (assets/css/design-system.css) mit 21 Komponenten,
  iPad-Responsive-Breakpoints, Touch-Ziele 36px, Music-Player,
  Glossar-Tooltips
- Templates (sims/template.html + student/teacher-Dashboard)
- Docs: module-interface.md (inkl. 4a Sprachregel, 4b Leichte Sprache,
  4c iPad), content-architecture.md, crash-recovery.md, music-registry.md
- Admin-Infrastruktur: admin-modules.html + api/admin-modules.php
  (Titel, Emoji, Bild, Status, Dauer, Alter pro Modul)
- Inbox-System: _inbox/README.md + _status.md fuer Atlas + Briefings
  an Klima, Glossar, Lehrplan, Fluss
- Zentrale SFX-Pipeline (scripts/generate-sounds.py)
- DALL-E-Bilder: 8 Badges + 5 Glossar-Repraesentationsbilder (Querformat)
- Logo + Inter-Font lokal
- PHP-APIs: admin, glossar, levels, licenses, progress, waypoints,
  assignments, profile, tickets
- Spielsprache entfernt (admin-modules, admin-levels, schueler)
- Landing-Page-Bearbeitungen (Boote sichtbarer, Button-Hintergrund)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:51:14 +02:00

395 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoGraSim — Modul-Verwaltung</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{background:#fff;padding:.6rem 1.2rem;display:flex;align-items:center;gap:.8rem;border-bottom:1px solid rgba(0,0,0,.06)}
.topbar h1{font-size:.95rem;font-weight:800;color:#4a7c8a;flex:1;display:flex;align-items:center;gap:.5rem}
.topbar h1 img{height:28px}
.btn-sm{padding:.3rem .6rem;border:1px solid rgba(0,0,0,.08);border-radius:6px;background:#fff;font-size:.68rem;font-weight:600;cursor:pointer;color:#4a4a4a;text-decoration:none}
.btn-sm:hover{background:#dae8ec;color:#4a7c8a}
.wrap{max-width:1200px;margin:1rem auto;padding:0 1rem}
.intro{color:#4a4a4a;font-size:.82rem;margin-bottom:1rem}
.module-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}
.module-card{background:#fff;border-radius:10px;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,.03);border:1px solid rgba(0,0,0,.04);display:flex;flex-direction:column}
.module-img{position:relative;aspect-ratio:16/10;background:#dae8ec;overflow:hidden}
.module-img img{width:100%;height:100%;object-fit:cover;display:block}
.module-img .no-img{display:flex;align-items:center;justify-content:center;height:100%;color:#8a8a8a;font-size:.72rem}
.module-img .status-badge{position:absolute;top:8px;right:8px;padding:3px 8px;border-radius:12px;font-size:.62rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em}
.status-aktiv{background:#dceadd;color:#3a6b3e}
.status-beta{background:#fff3cd;color:#8a6400}
.status-geplant{background:#e0e0e0;color:#666}
.status-archiv{background:#f2ddd8;color:#c07a6b}
.module-body{padding:.9rem;display:flex;flex-direction:column;gap:.6rem;flex:1}
.module-title{display:flex;align-items:center;gap:.5rem}
.module-emoji{font-size:1.8rem;line-height:1}
.module-title-text{font-size:1rem;font-weight:800;color:#1f4e5a;flex:1}
.module-sub{font-size:.74rem;color:#4a4a4a;line-height:1.35}
.module-meta{display:flex;flex-wrap:wrap;gap:.3rem;margin-top:.3rem}
.meta-chip{background:#f7f6f3;padding:2px 8px;border-radius:10px;font-size:.62rem;color:#4a4a4a}
.module-actions{display:flex;gap:.4rem;margin-top:.6rem}
.btn{padding:.45rem .9rem;border:none;border-radius:6px;background:#4a7c8a;color:#fff;font-size:.72rem;font-weight:700;cursor:pointer;font-family:inherit}
.btn:hover{background:#3a6470}
.btn-outline{background:transparent;color:#4a7c8a;border:1.5px solid #4a7c8a}
/* Edit Modal */
.overlay{position:fixed;inset:0;background:rgba(0,0,0,.45);display:none;align-items:flex-start;justify-content:center;z-index:100;padding:2rem;overflow-y:auto}
.overlay.open{display:flex}
.edit-card{background:#fff;border-radius:12px;max-width:600px;width:100%;padding:1.5rem;box-shadow:0 8px 30px rgba(0,0,0,.15)}
.edit-card h2{font-size:1.1rem;font-weight:800;color:#1f4e5a;margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}
.form-grid{display:grid;gap:.8rem}
.form-row{display:grid;grid-template-columns:auto 1fr;gap:.8rem;align-items:center}
.form-row label{font-size:.72rem;font-weight:600;color:#4a4a4a}
.form-row input,.form-row select,.form-row textarea{padding:.45rem .6rem;border:1.5px solid rgba(0,0,0,.1);border-radius:6px;font-size:.82rem;font-family:inherit;width:100%}
.form-row textarea{min-height:80px;resize:vertical}
.form-row .emoji-preview{display:inline-flex;align-items:center;gap:.4rem;background:#f7f6f3;padding:.35rem .6rem;border-radius:6px;font-size:1.4rem;min-width:2.5rem;justify-content:center}
.form-footer{display:flex;justify-content:flex-end;gap:.5rem;margin-top:1rem}
.emoji-picker{display:grid;grid-template-columns:repeat(10,1fr);gap:.3rem;margin-top:.4rem;max-height:140px;overflow-y:auto}
.emoji-pick{font-size:1.3rem;padding:.3rem;border:none;background:transparent;border-radius:6px;cursor:pointer}
.emoji-pick:hover{background:#dae8ec}
.upload-section{background:#f7f6f3;padding:.8rem;border-radius:8px}
.upload-section label{font-size:.72rem;font-weight:600;color:#4a4a4a;display:block;margin-bottom:.4rem}
.current-img{display:flex;align-items:center;gap:.6rem;margin-bottom:.6rem}
.current-img img{width:80px;height:50px;object-fit:cover;border-radius:4px}
.current-img .fname{font-family:monospace;font-size:.7rem;color:#8a8a8a}
.msg{font-size:.74rem;padding:.5rem .7rem;border-radius:6px;margin-top:.5rem}
.msg.ok{background:#dceadd;color:#3a6b3e}
.msg.err{background:#f2ddd8;color:#c07a6b}
.toolbar{display:flex;gap:.4rem;margin-bottom:1rem;flex-wrap:wrap;align-items:center}
.toolbar select{padding:.35rem .5rem;border:1.5px solid rgba(0,0,0,.1);border-radius:6px;font-size:.74rem;font-family:inherit}
</style>
</head>
<body>
<div class="topbar">
<h1><img src="assets/img/textlogo_geograsim.svg" alt="GeoGraSim"> Modul-Verwaltung (Admin)</h1>
<a href="admin-licenses.html" class="btn-sm">🎟️ Lizenzen</a>
<a href="admin-levels.html" class="btn-sm">🎮 Level</a>
<a href="admin.html" class="btn-sm">Logout</a>
</div>
<div class="wrap">
<p class="intro">Hier kannst du Titel, Emoji, Bild und Status aller Module anpassen. Änderungen werden sofort in die Datenbank geschrieben und sind überall in der App wirksam (Landing Page, Detailseiten, Dashboards).</p>
<div class="toolbar">
<label style="font-size:.72rem;font-weight:600">Filter:</label>
<select id="filter-status" onchange="render()">
<option value="">Alle Status</option>
<option value="aktiv">Aktiv</option>
<option value="beta">Beta</option>
<option value="geplant">Geplant</option>
<option value="archiv">Archiv</option>
</select>
</div>
<div id="grid" class="module-grid">
<p style="color:#8a8a8a;font-size:.8rem">Lade Module …</p>
</div>
</div>
<!-- Edit Modal -->
<div class="overlay" id="overlay" onclick="if(event.target===this)closeEdit()">
<div class="edit-card">
<h2><span id="edit-emoji-display">🧩</span> <span id="edit-title-display">Modul bearbeiten</span></h2>
<div class="form-grid">
<div class="form-row">
<label>Modul-ID</label>
<input id="f-id" readonly style="background:#f7f6f3;color:#8a8a8a">
</div>
<div class="form-row">
<label>Emoji</label>
<div>
<div style="display:flex;gap:.5rem;align-items:center">
<span class="emoji-preview" id="emoji-preview">🧩</span>
<input id="f-icon" maxlength="16" style="max-width:80px" oninput="document.getElementById('emoji-preview').textContent=this.value||'';document.getElementById('edit-emoji-display').textContent=this.value||'🧩'">
<button class="btn-sm" type="button" onclick="togglePicker()">🎨 Auswählen</button>
</div>
<div id="emoji-picker" class="emoji-picker" style="display:none"></div>
</div>
</div>
<div class="form-row">
<label>Titel</label>
<input id="f-title" maxlength="128" oninput="document.getElementById('edit-title-display').textContent=this.value||'Modul bearbeiten'">
</div>
<div class="form-row">
<label>Untertitel</label>
<input id="f-subtitle" maxlength="255">
</div>
<div class="form-row">
<label>Status</label>
<select id="f-status">
<option value="aktiv">Aktiv</option>
<option value="beta">Beta</option>
<option value="geplant">Geplant</option>
<option value="archiv">Archiv</option>
</select>
</div>
<div class="form-row">
<label>Dauer (min)</label>
<input id="f-duration" type="number" min="0">
</div>
<div class="form-row">
<label>Alter</label>
<div style="display:flex;gap:.4rem;align-items:center">
<input id="f-agemin" type="number" min="5" max="20" style="width:70px">
<span>bis</span>
<input id="f-agemax" type="number" min="5" max="20" style="width:70px">
<span style="font-size:.7rem;color:#8a8a8a">Jahre</span>
</div>
</div>
<div class="form-row">
<label>Simulations-URL</label>
<input id="f-playurl" placeholder="klima-2d">
</div>
<div class="form-row">
<label>Seiten-URL</label>
<input id="f-pageurl" placeholder="modul-klima">
</div>
<div class="form-row">
<label>Reihenfolge</label>
<input id="f-sort" type="number">
</div>
<div class="upload-section">
<label>Card-Bild</label>
<div class="current-img">
<img id="current-img-preview" src="" alt="" style="display:none">
<span class="fname" id="current-img-name">(kein Bild)</span>
</div>
<input type="file" id="f-image" accept="image/png,image/jpeg,image/webp,image/svg+xml">
<div style="font-size:.66rem;color:#8a8a8a;margin-top:.3rem">Empfohlen: 1024×640 PNG/JPG, max 5 MB</div>
</div>
</div>
<div id="edit-msg"></div>
<div class="form-footer">
<button class="btn btn-outline" onclick="closeEdit()">Abbrechen</button>
<button class="btn" onclick="save()">Speichern</button>
</div>
</div>
</div>
<script>
var BASE = location.pathname.replace(/\/[^\/]*$/, '');
var modules = [];
var editingId = null;
// Kuratierte Emoji-Auswahl passend zu GeoGraSim-Themen
var EMOJI_SET = ['🌍','🌎','🌏','🗺️','🧭','🏔️','⛰️','🌋','🏕️','🏞️',
'🌊','💧','🌧️','☀️','☁️','❄️','🌡️','🌪️','🌈','💨',
'🏙️','🏛️','🏘️','🏠','🏭','🚁','✈️','🚆','🚌','🚲',
'🌲','🌳','🌴','🌿','🍃','🌱','🐟','🐝','🦋','🐼',
'⚡','🔥','💡','🔋','☢️','🛢️','⛽','🌬️','♻️','🌿',
'📊','📈','📉','🎯','🏆','🎮','📚','🧪','🔬','🧬'];
function loadModules() {
fetch(BASE + '/api/admin-modules', { credentials: 'same-origin' })
.then(r => r.json())
.then(d => {
if (d.modules) {
modules = d.modules;
render();
} else {
document.getElementById('grid').innerHTML = '<p style="color:#c07a6b">Fehler: ' + (d.error || 'unbekannt') + '</p>';
}
});
}
function render() {
var filter = document.getElementById('filter-status').value;
var filtered = filter ? modules.filter(m => m.status === filter) : modules;
var grid = document.getElementById('grid');
if (!filtered.length) {
grid.innerHTML = '<p style="color:#8a8a8a;font-size:.8rem">Keine Module vorhanden.</p>';
return;
}
grid.innerHTML = filtered.map(m => {
var imgSrc = m.card_image ? 'assets/img/' + m.card_image : '';
var img = imgSrc
? '<img src="' + imgSrc + '" alt="' + esc(m.title) + '">'
: '<div class="no-img">Kein Bild</div>';
var dur = m.duration_min ? m.duration_min + ' min' : '—';
var age = (m.age_min && m.age_max) ? m.age_min + '' + m.age_max + ' J' : '—';
return '<div class="module-card">' +
'<div class="module-img">' + img +
'<div class="status-badge status-' + m.status + '">' + m.status + '</div></div>' +
'<div class="module-body">' +
'<div class="module-title"><span class="module-emoji">' + (m.icon || '🧩') + '</span>' +
'<span class="module-title-text">' + esc(m.title) + '</span></div>' +
(m.subtitle ? '<div class="module-sub">' + esc(m.subtitle) + '</div>' : '') +
'<div class="module-meta">' +
'<span class="meta-chip">🆔 ' + m.module_id + '</span>' +
'<span class="meta-chip">⏱ ' + dur + '</span>' +
'<span class="meta-chip">👥 ' + age + '</span>' +
'</div>' +
'<div class="module-actions">' +
'<button class="btn" onclick="editModule(\'' + m.module_id + '\')">Bearbeiten</button>' +
(m.play_url ? '<a class="btn btn-outline" href="' + m.play_url + '" target="_blank">▶ Testen</a>' : '') +
'</div>' +
'</div></div>';
}).join('');
}
function esc(s) { return (s || '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[c]); }
function editModule(id) {
var m = modules.find(x => x.module_id === id);
if (!m) return;
editingId = id;
document.getElementById('f-id').value = m.module_id;
document.getElementById('f-icon').value = m.icon || '';
document.getElementById('emoji-preview').textContent = m.icon || '';
document.getElementById('edit-emoji-display').textContent = m.icon || '🧩';
document.getElementById('f-title').value = m.title || '';
document.getElementById('edit-title-display').textContent = m.title || 'Modul bearbeiten';
document.getElementById('f-subtitle').value = m.subtitle || '';
document.getElementById('f-status').value = m.status || 'beta';
document.getElementById('f-duration').value = m.duration_min || '';
document.getElementById('f-agemin').value = m.age_min || '';
document.getElementById('f-agemax').value = m.age_max || '';
document.getElementById('f-playurl').value = m.play_url || '';
document.getElementById('f-pageurl').value = m.page_url || '';
document.getElementById('f-sort').value = m.sort_order || 0;
var imgPreview = document.getElementById('current-img-preview');
var imgName = document.getElementById('current-img-name');
if (m.card_image) {
imgPreview.src = 'assets/img/' + m.card_image;
imgPreview.style.display = '';
imgName.textContent = m.card_image;
} else {
imgPreview.style.display = 'none';
imgName.textContent = '(kein Bild)';
}
document.getElementById('f-image').value = '';
document.getElementById('edit-msg').innerHTML = '';
document.getElementById('overlay').classList.add('open');
renderEmojiPicker();
}
function renderEmojiPicker() {
var p = document.getElementById('emoji-picker');
p.innerHTML = EMOJI_SET.map(e =>
'<button type="button" class="emoji-pick" onclick="selectEmoji(\'' + e + '\')">' + e + '</button>'
).join('');
}
function togglePicker() {
var p = document.getElementById('emoji-picker');
p.style.display = p.style.display === 'none' ? 'grid' : 'none';
}
function selectEmoji(e) {
document.getElementById('f-icon').value = e;
document.getElementById('emoji-preview').textContent = e;
document.getElementById('edit-emoji-display').textContent = e;
document.getElementById('emoji-picker').style.display = 'none';
}
function closeEdit() {
document.getElementById('overlay').classList.remove('open');
editingId = null;
}
function save() {
if (!editingId) return;
var msg = document.getElementById('edit-msg');
msg.innerHTML = '';
var data = {
module_id: editingId,
title: document.getElementById('f-title').value.trim(),
subtitle: document.getElementById('f-subtitle').value.trim(),
icon: document.getElementById('f-icon').value.trim(),
status: document.getElementById('f-status').value,
duration_min: document.getElementById('f-duration').value || null,
age_min: document.getElementById('f-agemin').value || null,
age_max: document.getElementById('f-agemax').value || null,
play_url: document.getElementById('f-playurl').value.trim(),
page_url: document.getElementById('f-pageurl').value.trim(),
sort_order: document.getElementById('f-sort').value || 0
};
fetch(BASE + '/api/admin-modules', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(data)
})
.then(r => r.json())
.then(r => {
if (r.ok) {
// Wenn Bild hochgeladen: zusätzlich POST
var file = document.getElementById('f-image').files[0];
if (file) return uploadImage(file);
msg.innerHTML = '<div class="msg ok">✓ Gespeichert</div>';
loadModules();
setTimeout(closeEdit, 800);
} else {
msg.innerHTML = '<div class="msg err">Fehler: ' + (r.error || 'unbekannt') + '</div>';
}
})
.catch(e => { msg.innerHTML = '<div class="msg err">Netzwerkfehler</div>'; });
}
function uploadImage(file) {
var msg = document.getElementById('edit-msg');
var fd = new FormData();
fd.append('module_id', editingId);
fd.append('image', file);
return fetch(BASE + '/api/admin-modules', {
method: 'POST',
credentials: 'same-origin',
body: fd
})
.then(r => r.json())
.then(r => {
if (r.ok) {
msg.innerHTML = '<div class="msg ok">✓ Gespeichert inkl. Bild</div>';
loadModules();
setTimeout(closeEdit, 800);
} else {
msg.innerHTML = '<div class="msg err">Bild-Upload fehlgeschlagen: ' + (r.error || '') + '</div>';
}
});
}
// Auth-Check
fetch(BASE + '/api/admin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ action: 'status' })
})
.then(r => r.json())
.then(r => {
if (!r.authenticated) { location.href = 'admin.html'; return; }
loadModules();
});
</script>
</body>
</html>