Files
geograsim/App/assets/js/ggs-quick-intro.js
T
Adminator 496fc59c7b Busfahrt: Neustart-Button + Onboarding-Doppelung abgesichert
- ggs-quick-intro: open() entfernt vorab alle vorhandenen Overlays (nie 2x).
- Busfahrt: Neustart-Button (btn-reset, location.reload mit Rueckfrage).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-25 16:54:34 +02:00

189 lines
8.5 KiB
JavaScript
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.
/**
* GeoGraSim Plattform-Standard: Mini-Onboarding
* ==============================================
* Für Sims, die KEIN volles Tutorial haben aber Lernenden trotzdem einen
* kurzen Einstieg geben sollen: ein einfaches Modal mit Headline, Lead,
* 2-4 Bullet-Schritten und optionalen Lernzielen.
*
* USAGE in der Sim:
* <script src="/assets/js/ggs-quick-intro.js" defer></script>
* <script>
* window.addEventListener('DOMContentLoaded', () => {
* GgsQuickIntro.init({
* simId: 'busfahrt',
* title: '🚌 Busfahrt',
* lead: 'Du übernimmst Aufträge und transportierst Passagiere durch Europa.',
* steps: [
* 'Auftrag wählen (links) — du siehst Stadt, Distanz und Bezahlung.',
* 'Auf der Karte das richtige Ziel anklicken — Genauigkeit zählt.',
* 'Quiz beantworten — Hauptstadt + Land — sammelt Bonus.',
* 'Budget hochhalten, Aufträge gewinnen, Stufen freischalten.',
* ],
* goals: [
* 'Europäische Hauptstädte und Länder zuordnen',
* 'Distanzen einschätzen',
* 'Wirtschaftlich planen (Treibstoff, Marge)',
* ],
* });
* });
* </script>
*
* VERHALTEN:
* - Beim 1. Besuch (kein localStorage-Key) öffnet sich das Modal automatisch.
* - Schließen → localStorage merkt sich „gesehen" (key: ggs.intro.<simId>).
* - Registriert automatisch `window.GGS_TUTORIAL.replay()` als Plattform-API,
* damit der globale -Button im Header das Modal erneut öffnen kann.
* - „Anleitung erneut öffnen" verfügbar via `GgsQuickIntro.open()` oder
* `window.GGS_TUTORIAL.replay()`.
*
* STYLE: CSS wird automatisch injiziert. Mobile-first, iPad-tauglich,
* Esc + Tap-outside schließt.
*
* KOMPATIBILITÄT: Pure Vanilla-JS. Funktioniert mit existierendem
* Glossar-Tooltip-Pattern und GGS_TUTORIAL-Convention.
*/
(function () {
'use strict';
var CSS = ''
+ '.ggs-qi-back{position:fixed;inset:0;background:rgba(20,30,30,.55);display:flex;align-items:center;justify-content:center;z-index:9998;padding:2vw;animation:ggs-qi-fade .2s ease-out}'
+ '.ggs-qi-modal{background:#fff;border-radius:14px;max-width:520px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 18px 40px rgba(0,0,0,.28);font-family:inherit;animation:ggs-qi-pop .22s ease-out;-webkit-overflow-scrolling:touch}'
+ '.ggs-qi-head{padding:18px 22px 8px;border-bottom:1px solid rgba(0,0,0,.05)}'
+ '.ggs-qi-title{font-size:1.35rem;font-weight:800;color:#1f4b37;margin:0 0 .3rem}'
+ '.ggs-qi-lead{font-size:.96rem;color:#3a3a3a;margin:0;line-height:1.45}'
+ '.ggs-qi-body{padding:14px 22px 4px;font-size:.94rem;line-height:1.5;color:#2a2a2a}'
+ '.ggs-qi-h3{font-size:.78rem;text-transform:uppercase;letter-spacing:.04em;color:#5a5a5a;margin:14px 0 6px;font-weight:700}'
+ '.ggs-qi-steps{margin:0;padding:0 0 0 1.2rem;list-style:decimal}'
+ '.ggs-qi-steps li{margin:.35rem 0}'
+ '.ggs-qi-goals{margin:0;padding:0;list-style:none}'
+ '.ggs-qi-goals li{padding:.28rem 0 .28rem 1.4rem;position:relative}'
+ '.ggs-qi-goals li::before{content:"🎯";position:absolute;left:0;font-size:.92em}'
+ '.ggs-qi-foot{display:flex;justify-content:flex-end;gap:.6rem;padding:14px 22px 18px;border-top:1px solid rgba(0,0,0,.05);background:#fafaf6;border-radius:0 0 14px 14px;position:sticky;bottom:0}'
+ '.ggs-qi-btn{font-family:inherit;font-size:.94rem;font-weight:700;padding:.55rem 1.1rem;border-radius:8px;border:0;cursor:pointer;transition:transform .08s,background .15s}'
+ '.ggs-qi-btn-primary{background:#1f4b37;color:#fff}'
+ '.ggs-qi-btn-primary:hover{background:#173527}'
+ '.ggs-qi-btn-primary:active{transform:scale(.97)}'
+ '@keyframes ggs-qi-fade{from{opacity:0}to{opacity:1}}'
+ '@keyframes ggs-qi-pop{from{opacity:0;transform:translateY(12px) scale(.97)}to{opacity:1;transform:none}}'
+ '@media (max-width:520px){.ggs-qi-modal{max-height:96vh}.ggs-qi-title{font-size:1.18rem}.ggs-qi-foot{padding-bottom:max(18px,env(safe-area-inset-bottom))}}';
var CSS_INJECTED = false;
function injectCss() {
if (CSS_INJECTED) return;
var s = document.createElement('style');
s.id = 'ggs-quick-intro-css';
s.textContent = CSS;
document.head.appendChild(s);
CSS_INJECTED = true;
}
function escapeHtml(s) {
return String(s == null ? '' : s).replace(/[<>&"]/g, function (c) {
return { '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' }[c];
});
}
var _config = null;
var _back = null;
var _page = 1;
function lsKey() { return _config && _config.simId ? 'ggs.intro.' + _config.simId : null; }
function markSeen() { try { var k = lsKey(); if (k) localStorage.setItem(k, '1'); } catch (_) {} }
function isSeen() { try { var k = lsKey(); return k && localStorage.getItem(k) === '1'; } catch (_) { return false; } }
function close() {
if (!_back) return;
_back.remove();
_back = null;
document.removeEventListener('keydown', onKey);
}
function onKey(e) { if (e.key === 'Escape') close(); }
function onBackClick(e) {
if (e.target === _back) { markSeen(); close(); return; }
var a = e.target && e.target.dataset && e.target.dataset.action;
if (a === 'next') { _page = 2; render(); }
else if (a === 'back') { _page = 1; render(); }
else if (a === 'start') { markSeen(); close(); }
}
function render() {
injectCss();
var p2 = _config.page2 || null;
var hasP2 = !!(p2 && (p2.html || p2.body));
var headTitle, headLead, body, foot;
if (_page === 2 && hasP2) {
headTitle = p2.title || _config.title || 'Mehr dazu';
headLead = '';
body = (p2.html || p2.body || '');
foot = '<button type="button" class="ggs-qi-btn" data-action="back" style="background:#e8e4d8;color:#2a2a2a">← Zurück</button>'
+ '<button type="button" class="ggs-qi-btn ggs-qi-btn-primary" data-action="start">Los geht\'s →</button>';
} else {
var steps = (_config.steps || []).map(function (s) {
return '<li>' + (s.indexOf('<') >= 0 ? s : escapeHtml(s)) + '</li>';
}).join('');
var goals = (_config.goals || []).map(function (g) {
return '<li>' + escapeHtml(g) + '</li>';
}).join('');
headTitle = _config.title || 'Anleitung';
headLead = _config.lead ? '<p class="ggs-qi-lead">' + escapeHtml(_config.lead) + '</p>' : '';
body = (steps ? '<div class="ggs-qi-h3">Was tust du?</div><ol class="ggs-qi-steps">' + steps + '</ol>' : '')
+ (goals ? '<div class="ggs-qi-h3">Was lernst du?</div><ul class="ggs-qi-goals">' + goals + '</ul>' : '');
foot = hasP2
? '<button type="button" class="ggs-qi-btn ggs-qi-btn-primary" data-action="next">Weiter →</button>'
: '<button type="button" class="ggs-qi-btn ggs-qi-btn-primary" data-action="start">Los geht\'s →</button>';
}
var html = ''
+ '<div class="ggs-qi-modal" role="dialog" aria-modal="true" aria-labelledby="ggs-qi-title">'
+ '<div class="ggs-qi-head">'
+ '<h2 id="ggs-qi-title" class="ggs-qi-title">' + escapeHtml(headTitle) + '</h2>'
+ headLead
+ '</div>'
+ '<div class="ggs-qi-body">' + body + '</div>'
+ '<div class="ggs-qi-foot">' + foot + '</div>'
+ '</div>';
if (!_back) {
_back = document.createElement('div');
_back.className = 'ggs-qi-back';
_back.addEventListener('click', onBackClick);
document.body.appendChild(_back);
document.addEventListener('keydown', onKey);
}
_back.innerHTML = html;
var m = _back.querySelector('.ggs-qi-modal'); if (m) m.scrollTop = 0;
}
function open() {
if (!_config) return;
// Sicherheitsnetz: alle evtl. schon vorhandenen Overlays entfernen (nie doppelt)
var olds = document.querySelectorAll('.ggs-qi-back');
for (var i = 0; i < olds.length; i++) olds[i].remove();
_back = null;
document.removeEventListener('keydown', onKey);
_page = 1;
render();
}
window.GgsQuickIntro = {
init: function (config) {
_config = config || {};
// Plattform-Convention: GGS_TUTORIAL.replay() wird vom -Button im Header gerufen.
window.GGS_TUTORIAL = {
simId: _config.simId,
title: _config.title,
replay: function () { open(); },
};
// Beim 1. Besuch automatisch öffnen.
if (!isSeen()) {
// Kleiner Delay, damit Sim-Boot zuerst sein DOM aufbauen kann.
setTimeout(open, 400);
}
},
open: open,
close: close,
};
})();