From 30044e00e975a65485c679a05ac52058a31c7110 Mon Sep 17 00:00:00 2001 From: patrickzerhusen Date: Thu, 11 Jun 2026 16:38:25 +0200 Subject: [PATCH] progress bar skip button, labeled arrow hint --- public/js/app.js | 10 +++ public/js/onboarding.js | 189 +++++++++++++++++++--------------------- public/styles.css | 91 +++++++++++++++++++ 3 files changed, 193 insertions(+), 97 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index 8df37cd..15ec4d6 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -955,6 +955,11 @@ function submitLogin() { document.cookie = 'webgis_user=' + encodeURIComponent(name) + ';path=/;max-age=31536000;SameSite=Lax'; document.getElementById('login-modal').style.display = 'none'; + // Starts Onboarding Tour on first Login + if (!localStorage.getItem('webgis_onboarding_done')) { + setTimeout(function () { startTour(); }, 500); + } + // Open Create Modal if Geometry is pending if (drawnGeometry) { document.getElementById('create-geom').value = JSON.stringify(drawnGeometry); @@ -965,6 +970,11 @@ function submitLogin() { function skipLogin() { document.getElementById('login-modal').style.display = 'none'; + + // Starts Onboarding Tour on first Login + if (!localStorage.getItem('webgis_onboarding_done')) { + setTimeout(function () { startTour(); }, 500); + } } // Info Modal diff --git a/public/js/onboarding.js b/public/js/onboarding.js index eff5ccd..fce1845 100644 --- a/public/js/onboarding.js +++ b/public/js/onboarding.js @@ -9,12 +9,6 @@ // Block 1: Onboarding Configuration // ================================================================= -// ONBOARDING_MODE — Controls when the Tutorial is shown: -const ONBOARDING_MODE = 'once'; -// 'once' — Shown on first Visit, stored in localStorage -// 'session' — Shown per Browser Session, stored in sessionStorage -// 'always' — Shows always, nothing stored - // Prevents double Initialization let onboardingStarted = false; @@ -25,57 +19,10 @@ function isMobile() { // ================================================================= -// Block 2: Tour Initialization +// Block 2: Tour Definition // ================================================================= -function initOnboardingTour() { - - // Checks if Tutorial should be shown based on Onboarding Mode - if (ONBOARDING_MODE === 'once' && localStorage.getItem('webgis_onboarding_done')) { - return; - } - if (ONBOARDING_MODE === 'session' && sessionStorage.getItem('webgis_onboarding_done')) { - return; - } - - // Waits for Welcome and Login Modals to be closed - waitForModalsToClose(function () { - setTimeout(startTour, 600); - }); -} - - -// ================================================================= -// Block 3: Modal Watcher — Starts Tour after Welcome and Login Modals closed -// ================================================================= - -function waitForModalsToClose(callback) { - const welcomeModal = document.getElementById('welcome-modal'); - const loginModal = document.getElementById('login-modal'); - - const checkInterval = setInterval(function () { - const welcomeHidden = !welcomeModal || welcomeModal.style.display === 'none' || welcomeModal.style.display === ''; - const loginHidden = !loginModal || loginModal.style.display === 'none' || loginModal.style.display === ''; - - if (welcomeHidden && loginHidden) { - clearInterval(checkInterval); - callback(); - } - }, 300); - - // Safety Timeout - setTimeout(function () { - clearInterval(checkInterval); - callback(); - }, 30000); -} - - -// ================================================================= -// Block 4: Tour Definition -// ================================================================= - -function startTour() { +function startTour(manual) { // Prevents double Start if (onboardingStarted) return; onboardingStarted = true; @@ -97,26 +44,45 @@ function startTour() { }); +// ----------------------------------------------------------------- + // Step 1: Welcome — Skip Timer at automatic Start // ----------------------------------------------------------------- - // Step 1: Welcome (identical on Mobile and Desktop) - // ----------------------------------------------------------------- + var welcomeButtons = [ + { + text: 'Überspringen', + action: tour.cancel, + classes: 'shepherd-button-secondary' + (manual ? '' : ' skip-btn-locked') + }, + { + text: 'Los geht\'s ', + action: tour.next, + classes: 'shepherd-button-primary' + } + ]; + tour.addStep({ id: 'welcome', title: ' Willkommen bei der Mitmachkarte!', text: 'Dieses interaktive Tutorial zeigt Ihnen die Kernfunktionen der Mitmachkarte.' + - '

Sie können das Tutorial jederzeit über den Hilfe-Tab der Seitenleiste wiederholen.', - buttons: [ - { - text: 'Überspringen', - action: tour.cancel, - classes: 'shepherd-button-secondary' - }, - { - text: 'Los geht\'s ', - action: tour.next, - classes: 'shepherd-button-primary' + '

Sie können das Tutorial jederzeit über den Hilfe-Tab der Seitenleiste wiederholen.', + buttons: welcomeButtons, + when: { + show: function () { + if (manual) return; + + // Locks Skip Button with Progress Bar for 5 Seconds + var skipBtn = document.querySelector('.skip-btn-locked'); + if (!skipBtn) return; + skipBtn.disabled = true; + skipBtn.style.pointerEvents = 'none'; + + setTimeout(function () { + skipBtn.disabled = false; + skipBtn.style.pointerEvents = ''; + skipBtn.classList.remove('skip-btn-locked'); + }, 5000); } - ] + } }); @@ -233,7 +199,7 @@ function startTour() { classes: 'shepherd-button-secondary' }, { - text: 'Beenden ', + text: 'Abschließen ', action: tour.next, classes: 'shepherd-button-primary' } @@ -259,51 +225,80 @@ function startTour() { // ----------------------------------------------------------------- - // Completion and Cancellation + // Completion and Cancellation — shows Drawing Arrow // ----------------------------------------------------------------- - tour.on('complete', function () { - markOnboardingDone(); + function onTourEnd() { onboardingStarted = false; if (mobile) sidebar.close(); - }); - tour.on('cancel', function () { - markOnboardingDone(); - onboardingStarted = false; - if (mobile) sidebar.close(); - }); + // Shows Arrow Hint + if (!localStorage.getItem('webgis_onboarding_done')) { + localStorage.setItem('webgis_onboarding_done', 'true'); + showDrawingArrow(); + } + } + + tour.on('complete', onTourEnd); + tour.on('cancel', onTourEnd); tour.start(); } // ================================================================= -// Marks Onboarding as completed +// Drawing Arrow — Points to Geoman Toolbar after Tour // ================================================================= -function markOnboardingDone() { - if (ONBOARDING_MODE === 'once') { - localStorage.setItem('webgis_onboarding_done', 'true'); - } else if (ONBOARDING_MODE === 'session') { - sessionStorage.setItem('webgis_onboarding_done', 'true'); +function showDrawingArrow() { + var hint = document.createElement('div'); + hint.id = 'drawing-hint-arrow'; + hint.innerHTML = '' + + ' Beitrag hinzufügen' + + '' + + '' + + '' + + '' + + ''; + document.body.appendChild(hint); + + // Positions Hint centered on Geoman Toolbar + function positionHint() { + var toolbar = document.querySelector('.leaflet-pm-toolbar'); + if (!toolbar) { removeDrawingArrow(); return; } + + var rect = toolbar.getBoundingClientRect(); + var hintHeight = hint.offsetHeight || 32; + hint.style.top = (rect.top + (rect.height / 2) - (hintHeight / 2)) + 'px'; + hint.style.right = (window.innerWidth - rect.left + 10) + 'px'; + } + + positionHint(); + window.addEventListener('resize', positionHint); + + var timeout = setTimeout(removeDrawingArrow, 60000); + + map.on('pm:globaldrawmodetoggled', function onDraw() { + clearTimeout(timeout); + removeDrawingArrow(); + map.off('pm:globaldrawmodetoggled', onDraw); + window.removeEventListener('resize', positionHint); + }); +} + +function removeDrawingArrow() { + var arrow = document.getElementById('drawing-hint-arrow'); + if (arrow) { + arrow.classList.add('fade-out'); + setTimeout(function () { arrow.remove(); }, 300); } } // ================================================================= -// Manual Tour Restart +// Manual Tour Restart (from Info Modal or Help Tab) // ================================================================= function restartOnboarding() { - localStorage.removeItem('webgis_onboarding_done'); - sessionStorage.removeItem('webgis_onboarding_done'); onboardingStarted = false; - startTour(); -} - - -// ================================================================= -// Auto-Start on Page Load -// ================================================================= - -initOnboardingTour(); \ No newline at end of file + startTour(true); +} \ No newline at end of file diff --git a/public/styles.css b/public/styles.css index 32c9935..9116e55 100644 --- a/public/styles.css +++ b/public/styles.css @@ -875,6 +875,97 @@ select.form-input { cursor: pointer; } transform: translate(-50%, -50%) !important; } + +/* ----------------------------------------------------------------- + 4.10 Skip Button Progress Bar (Onboarding) + ----------------------------------------------------------------- */ +.skip-btn-locked { + position: relative; + overflow: hidden; + opacity: 0.6; +} + +.skip-btn-locked::after { + content: ''; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 0%; + background: rgba(0, 0, 0, 0.12); + border-radius: 6px; + animation: skip-fill 5s linear forwards; + pointer-events: none; +} + +@keyframes skip-fill { + from { width: 0%; } + to { width: 100%; opacity: 0; } +} + + +/* ----------------------------------------------------------------- + 4.11 Drawing Hint Arrow after Onboarding + ----------------------------------------------------------------- */ +#drawing-hint-arrow { + position: fixed; + z-index: 1500; + display: flex; + align-items: center; + gap: 4px; + pointer-events: none; + animation: hint-fade-in 400ms ease; + transition: opacity 300ms ease; +} + +#drawing-hint-arrow.fade-out { + opacity: 0; +} + +.drawing-hint-label { + background: var(--color-primary); + color: white; + font-family: var(--font-body); + font-size: 0.78rem; + font-weight: 600; + padding: 6px 14px; + border-radius: 20px; + white-space: nowrap; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + display: flex; + align-items: center; + gap: 6px; +} + +.drawing-hint-chevrons { + display: flex; + gap: 0; + color: var(--color-primary); + font-size: 1.2rem; + font-weight: 900; + filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.2)); +} + +.drawing-hint-chevrons i:nth-child(1) { + animation: chevron-pulse 1.2s ease-in-out infinite; + opacity: 0.4; +} + +.drawing-hint-chevrons i:nth-child(2) { + animation: chevron-pulse 1.2s ease-in-out 0.3s infinite; + opacity: 0.7; +} + +@keyframes chevron-pulse { + 0%, 100% { transform: translateX(0); opacity: 0.3; } + 50% { transform: translateX(3px); opacity: 1; } +} + +@keyframes hint-fade-in { + from { opacity: 0; transform: translateX(-10px); } + to { opacity: 1; transform: translateX(0); } +} + /* Mobile */ @media (max-width: 768px) { .shepherd-element { max-width: 300px !important; }