Files
geograsim/App/assets/js/ggs-quick-intro.js
T
Adminator 8f89613eee Busfahrt: Auftrags-Card tiefer, EN-Button -> Onboarding Seite 2, Texte
- ggs-quick-intro.js: optionale zweite Seite (config.page2 {title, html})
  mit Weiter/Zurueck-Navigation. Rueckwaertskompatibel (ohne page2 = 1 Seite).
- Busfahrt: EN-Button entfernt; die "Warum Englisch?"-Erklaerung liegt jetzt
  auf Onboarding-Seite 2 + Satz zur mehrsprachigen Hintergrundmusik.
- Onboarding-Schritte ohne lange "-" ausformuliert.
- Auftrags-Card (.bf-order) 20px tiefer (top 84).

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

185 lines
8.2 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;
close();
_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,
};
})();