Skip to content

Instantly share code, notes, and snippets.

@schuhwerk
Created May 5, 2026 16:02
Show Gist options
  • Select an option

  • Save schuhwerk/fd6f8a263852dc0e23159fa9ed0e5adf to your computer and use it in GitHub Desktop.

Select an option

Save schuhwerk/fd6f8a263852dc0e23159fa9ed0e5adf to your computer and use it in GitHub Desktop.
onlineag-userscript-tour.js
// ==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