Files
geograsim/App/assets/js/ggs-quick-intro.js
T
Adminator 1dffef8200 Busfahrt: nur EIN Onboarding (Welcome-Overlay abgeloest)
Ursache 2x: Busfahrt zeigte sein eigenes #welcome-overlay UND das
GgsQuickIntro-Onboarding. Jetzt ist GgsQuickIntro das einzige Start-Gate:
- ggs-quick-intro: onStart-Callback (feuert einmal beim Schliessen bzw.
  sofort wenn schon gesehen), isSeen exportiert.
- Busfahrt: welcome-overlay wird nicht mehr gezeigt; onStart -> startGame;
  startGame wartet per Retry auf boot() (kein Timing-Problem).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-25 17:09:28 +02:00

204 lines
9.0 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;
var _startFired = false;
// onStart wird EINMAL ausgelöst — wenn das Onboarding geschlossen wird
// (oder sofort, falls schon gesehen). Sims können daran den Spielstart hängen.
function fireStart() {
if (_startFired) return;
if (_config && typeof _config.onStart === 'function') {
_startFired = true;
try { _config.onStart(); } catch (_) {}
}
}
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') { markSeen(); close(); fireStart(); } }
function onBackClick(e) {
if (e.target === _back) { markSeen(); close(); fireStart(); 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(); fireStart(); }
}
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);
} else {
// Schon gesehen: kein Onboarding, aber die Sim darf trotzdem starten.
fireStart();
}
},
open: open,
close: close,
isSeen: isSeen,
};
})();