Created
May 5, 2026 16:02
-
-
Save schuhwerk/fd6f8a263852dc0e23159fa9ed0e5adf to your computer and use it in GitHub Desktop.
onlineag-userscript-tour.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Guided Tour | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.0 | |
| // @description Adds a guided tour to your website with persistent state | |
| // @match https://spaces.kisd.de/* | |
| // @grant GM_setValue | |
| // @grant GM_getValue | |
| // @run-at document-end | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| // ============================================================ | |
| // EDIT ONLY THIS SECTION | |
| // ============================================================ | |
| const TOUR_ID = 'main-tour-v1'; // bump this to re-show after changes | |
| const AUTO_START = true; // start on first visit automatically | |
| const SHOW_REPLAY_BUTTON = true; // floating "Take tour" button | |
| const STEPS = [ | |
| { | |
| selector: '.menu-anchor, header, nav, .navbar', // CSS selector for target | |
| title: 'Navigation', | |
| text: 'Use this menu to move between sections of the site.', | |
| position: 'bottom', // top | bottom | left | right | |
| }, | |
| { | |
| selector: 'main, #content, .content', | |
| title: 'Main content', | |
| text: 'This is where the main content of the page lives.', | |
| position: 'top', | |
| }, | |
| { | |
| selector: 'footer', | |
| title: 'Footer', | |
| text: 'Find contact info and links down here.', | |
| position: 'top', | |
| }, | |
| ]; | |
| // ============================================================ | |
| // END OF EDIT SECTION | |
| // ============================================================ | |
| const STORAGE_KEY = `guided-tour:${TOUR_ID}:completed`; | |
| // Storage helpers — works with or without GM_* APIs | |
| const storage = { | |
| get(key) { | |
| try { | |
| if (typeof GM_getValue === 'function') { | |
| return GM_getValue(key, null); | |
| } | |
| } catch (e) {} | |
| return localStorage.getItem(key); | |
| }, | |
| set(key, val) { | |
| try { | |
| if (typeof GM_setValue === 'function') { | |
| GM_setValue(key, val); | |
| return; | |
| } | |
| } catch (e) {} | |
| localStorage.setItem(key, val); | |
| }, | |
| }; | |
| // Inject styles | |
| const style = document.createElement('style'); | |
| style.textContent = ` | |
| .gt-overlay { | |
| position: fixed; inset: 0; | |
| background: rgba(0,0,0,0.5); | |
| z-index: 999998; | |
| pointer-events: auto; | |
| } | |
| .gt-highlight { | |
| position: absolute; | |
| border: 3px solid #4f8cff; | |
| border-radius: 6px; | |
| box-shadow: 0 0 0 9999px rgba(0,0,0,0.5); | |
| z-index: 999999; | |
| pointer-events: none; | |
| transition: all 0.25s ease; | |
| } | |
| .gt-tooltip { | |
| position: absolute; | |
| background: #fff; | |
| color: #1a1a1a; | |
| padding: 16px 18px; | |
| border-radius: 8px; | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.25); | |
| max-width: 320px; | |
| min-width: 240px; | |
| z-index: 1000000; | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| font-size: 14px; | |
| line-height: 1.5; | |
| transition: all 0.25s ease; | |
| } | |
| .gt-tooltip h3 { | |
| margin: 0 0 8px 0; | |
| font-size: 16px; | |
| font-weight: 600; | |
| color: #1a1a1a; | |
| } | |
| .gt-tooltip p { | |
| margin: 0 0 14px 0; | |
| color: #444; | |
| } | |
| .gt-tooltip-footer { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 10px; | |
| } | |
| .gt-progress { | |
| font-size: 12px; | |
| color: #888; | |
| } | |
| .gt-buttons { display: flex; gap: 8px; } | |
| .gt-btn { | |
| border: none; | |
| padding: 7px 14px; | |
| border-radius: 5px; | |
| font-size: 13px; | |
| cursor: pointer; | |
| font-family: inherit; | |
| font-weight: 500; | |
| transition: background 0.15s; | |
| } | |
| .gt-btn-primary { background: #4f8cff; color: #fff; } | |
| .gt-btn-primary:hover { background: #3a76e8; } | |
| .gt-btn-secondary { background: #eee; color: #333; } | |
| .gt-btn-secondary:hover { background: #ddd; } | |
| .gt-skip { | |
| position: absolute; | |
| top: 8px; right: 10px; | |
| background: none; border: none; | |
| font-size: 18px; line-height: 1; | |
| color: #999; cursor: pointer; | |
| padding: 2px 6px; | |
| } | |
| .gt-skip:hover { color: #333; } | |
| .gt-replay { | |
| position: fixed; | |
| bottom: 20px; right: 20px; | |
| background: #4f8cff; | |
| color: #fff; | |
| border: none; | |
| padding: 10px 16px; | |
| border-radius: 999px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.2); | |
| cursor: pointer; | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| font-size: 13px; | |
| font-weight: 500; | |
| z-index: 999997; | |
| } | |
| .gt-replay:hover { background: #3a76e8; } | |
| `; | |
| document.head.appendChild(style); | |
| let currentStep = 0; | |
| let elements = {}; | |
| function findTarget(selector) { | |
| const candidates = selector.split(',').map(s => s.trim()); | |
| for (const sel of candidates) { | |
| const el = document.querySelector(sel); | |
| if (el) return el; | |
| } | |
| return null; | |
| } | |
| function positionTooltip(target, tooltip, preferred) { | |
| const rect = target.getBoundingClientRect(); | |
| const tipRect = tooltip.getBoundingClientRect(); | |
| const margin = 12; | |
| const scrollY = window.scrollY; | |
| const scrollX = window.scrollX; | |
| let top, left; | |
| const positions = { | |
| bottom: () => ({ | |
| top: rect.bottom + scrollY + margin, | |
| left: rect.left + scrollX + (rect.width - tipRect.width) / 2, | |
| }), | |
| top: () => ({ | |
| top: rect.top + scrollY - tipRect.height - margin, | |
| left: rect.left + scrollX + (rect.width - tipRect.width) / 2, | |
| }), | |
| right: () => ({ | |
| top: rect.top + scrollY + (rect.height - tipRect.height) / 2, | |
| left: rect.right + scrollX + margin, | |
| }), | |
| left: () => ({ | |
| top: rect.top + scrollY + (rect.height - tipRect.height) / 2, | |
| left: rect.left + scrollX - tipRect.width - margin, | |
| }), | |
| }; | |
| const pos = (positions[preferred] || positions.bottom)(); | |
| top = pos.top; | |
| left = pos.left; | |
| // Keep on screen horizontally | |
| const maxLeft = scrollX + window.innerWidth - tipRect.width - 10; | |
| const minLeft = scrollX + 10; | |
| left = Math.max(minLeft, Math.min(left, maxLeft)); | |
| tooltip.style.top = `${top}px`; | |
| tooltip.style.left = `${left}px`; | |
| } | |
| function positionHighlight(target, highlight) { | |
| const rect = target.getBoundingClientRect(); | |
| highlight.style.top = `${rect.top + window.scrollY - 4}px`; | |
| highlight.style.left = `${rect.left + window.scrollX - 4}px`; | |
| highlight.style.width = `${rect.width + 8}px`; | |
| highlight.style.height = `${rect.height + 8}px`; | |
| } | |
| function showStep(index) { | |
| const step = STEPS[index]; | |
| if (!step) return endTour(true); | |
| const target = findTarget(step.selector); | |
| if (!target) { | |
| // Skip steps whose target doesn't exist on this page | |
| console.warn(`[Guided Tour] Target not found for step ${index}: ${step.selector}`); | |
| return showStep(index + 1); | |
| } | |
| target.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| setTimeout(() => { | |
| positionHighlight(target, elements.highlight); | |
| elements.tooltipTitle.textContent = step.title; | |
| elements.tooltipText.textContent = step.text; | |
| elements.progress.textContent = `${index + 1} / ${STEPS.length}`; | |
| elements.nextBtn.textContent = index === STEPS.length - 1 ? 'Done' : 'Next'; | |
| elements.prevBtn.style.visibility = index === 0 ? 'hidden' : 'visible'; | |
| positionTooltip(target, elements.tooltip, step.position); | |
| }, 300); | |
| } | |
| function endTour(completed) { | |
| if (completed) { | |
| storage.set(STORAGE_KEY, '1'); | |
| } | |
| Object.values(elements).forEach(el => el && el.remove && el.remove()); | |
| elements = {}; | |
| window.removeEventListener('resize', handleResize); | |
| window.removeEventListener('scroll', handleResize, true); | |
| } | |
| function handleResize() { | |
| const step = STEPS[currentStep]; | |
| if (!step) return; | |
| const target = findTarget(step.selector); | |
| if (target && elements.highlight) { | |
| positionHighlight(target, elements.highlight); | |
| positionTooltip(target, elements.tooltip, step.position); | |
| } | |
| } | |
| function startTour() { | |
| currentStep = 0; | |
| const overlay = document.createElement('div'); | |
| overlay.className = 'gt-overlay'; | |
| const highlight = document.createElement('div'); | |
| highlight.className = 'gt-highlight'; | |
| const tooltip = document.createElement('div'); | |
| tooltip.className = 'gt-tooltip'; | |
| tooltip.innerHTML = ` | |
| <button class="gt-skip" aria-label="Skip tour">×</button> | |
| <h3 class="gt-tooltip-title"></h3> | |
| <p class="gt-tooltip-text"></p> | |
| <div class="gt-tooltip-footer"> | |
| <span class="gt-progress"></span> | |
| <div class="gt-buttons"> | |
| <button class="gt-btn gt-btn-secondary gt-prev">Back</button> | |
| <button class="gt-btn gt-btn-primary gt-next">Next</button> | |
| </div> | |
| </div> | |
| `; | |
| document.body.appendChild(overlay); | |
| document.body.appendChild(highlight); | |
| document.body.appendChild(tooltip); | |
| elements = { | |
| overlay, | |
| highlight, | |
| tooltip, | |
| tooltipTitle: tooltip.querySelector('.gt-tooltip-title'), | |
| tooltipText: tooltip.querySelector('.gt-tooltip-text'), | |
| progress: tooltip.querySelector('.gt-progress'), | |
| nextBtn: tooltip.querySelector('.gt-next'), | |
| prevBtn: tooltip.querySelector('.gt-prev'), | |
| skipBtn: tooltip.querySelector('.gt-skip'), | |
| }; | |
| elements.nextBtn.addEventListener('click', () => { | |
| currentStep++; | |
| showStep(currentStep); | |
| }); | |
| elements.prevBtn.addEventListener('click', () => { | |
| currentStep = Math.max(0, currentStep - 1); | |
| showStep(currentStep); | |
| }); | |
| elements.skipBtn.addEventListener('click', () => endTour(true)); | |
| overlay.addEventListener('click', () => endTour(true)); | |
| window.addEventListener('resize', handleResize); | |
| window.addEventListener('scroll', handleResize, true); | |
| showStep(0); | |
| } | |
| function addReplayButton() { | |
| const btn = document.createElement('button'); | |
| btn.className = 'gt-replay'; | |
| btn.textContent = '? Take tour'; | |
| btn.addEventListener('click', () => { | |
| if (Object.keys(elements).length === 0) startTour(); | |
| }); | |
| document.body.appendChild(btn); | |
| } | |
| // Init | |
| function init() { | |
| const completed = storage.get(STORAGE_KEY); | |
| if (AUTO_START && !completed) { | |
| // Small delay to let page settle | |
| setTimeout(startTour, 500); | |
| } | |
| if (SHOW_REPLAY_BUTTON) { | |
| addReplayButton(); | |
| } | |
| } | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', init); | |
| } else { | |
| init(); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment