326 Zeilen
8.1 KiB
JavaScript
326 Zeilen
8.1 KiB
JavaScript
/**
|
|
* TASKMATE - Tour/Onboarding Module
|
|
* ==================================
|
|
* First-time user onboarding tour
|
|
*/
|
|
|
|
import { $, createElement } from './utils.js';
|
|
|
|
class TourManager {
|
|
constructor() {
|
|
this.currentStep = 0;
|
|
this.isActive = false;
|
|
this.overlay = null;
|
|
this.tooltip = null;
|
|
|
|
this.steps = [
|
|
{
|
|
target: '.view-tabs',
|
|
title: 'Ansichten',
|
|
content: 'Wechseln Sie zwischen Board-, Listen-, Kalender- und Dashboard-Ansicht.',
|
|
position: 'bottom'
|
|
},
|
|
{
|
|
target: '.project-selector',
|
|
title: 'Projekte',
|
|
content: 'Wählen Sie ein Projekt aus oder erstellen Sie ein neues.',
|
|
position: 'bottom'
|
|
},
|
|
{
|
|
target: '.column',
|
|
title: 'Spalten',
|
|
content: 'Spalten repräsentieren den Status Ihrer Aufgaben. Ziehen Sie Aufgaben zwischen Spalten, um den Status zu ändern.',
|
|
position: 'right'
|
|
},
|
|
{
|
|
target: '.btn-add-task',
|
|
title: 'Neue Aufgabe',
|
|
content: 'Klicken Sie hier, um eine neue Aufgabe zu erstellen.',
|
|
position: 'top'
|
|
},
|
|
{
|
|
target: '.filter-bar',
|
|
title: 'Filter',
|
|
content: 'Filtern Sie Aufgaben nach Priorität, Bearbeiter oder Fälligkeitsdatum.',
|
|
position: 'bottom'
|
|
},
|
|
{
|
|
target: '#search-input',
|
|
title: 'Suche',
|
|
content: 'Durchsuchen Sie alle Aufgaben nach Titel oder Beschreibung. Tipp: Drücken Sie "/" für schnellen Zugriff.',
|
|
position: 'bottom'
|
|
},
|
|
{
|
|
target: '.user-menu',
|
|
title: 'Benutzermenu',
|
|
content: 'Hier können Sie Ihr Passwort ändern oder sich abmelden.',
|
|
position: 'bottom-left'
|
|
},
|
|
{
|
|
target: '#theme-toggle',
|
|
title: 'Design',
|
|
content: 'Wechseln Sie zwischen hellem und dunklem Design.',
|
|
position: 'bottom-left'
|
|
}
|
|
];
|
|
|
|
this.bindEvents();
|
|
}
|
|
|
|
bindEvents() {
|
|
window.addEventListener('tour:start', () => this.start());
|
|
window.addEventListener('tour:stop', () => this.stop());
|
|
|
|
// Keyboard navigation
|
|
document.addEventListener('keydown', (e) => {
|
|
if (!this.isActive) return;
|
|
|
|
if (e.key === 'Escape') {
|
|
this.stop();
|
|
} else if (e.key === 'ArrowRight' || e.key === 'Enter') {
|
|
this.next();
|
|
} else if (e.key === 'ArrowLeft') {
|
|
this.previous();
|
|
}
|
|
});
|
|
}
|
|
|
|
// =====================
|
|
// TOUR CONTROL
|
|
// =====================
|
|
|
|
start() {
|
|
// Check if tour was already completed
|
|
if (localStorage.getItem('tour_completed') === 'true') {
|
|
return;
|
|
}
|
|
|
|
this.isActive = true;
|
|
this.currentStep = 0;
|
|
|
|
this.createOverlay();
|
|
this.showStep();
|
|
}
|
|
|
|
stop() {
|
|
this.isActive = false;
|
|
|
|
if (this.overlay) {
|
|
this.overlay.remove();
|
|
this.overlay = null;
|
|
}
|
|
|
|
if (this.tooltip) {
|
|
this.tooltip.remove();
|
|
this.tooltip = null;
|
|
}
|
|
|
|
// Remove highlight from any element
|
|
$$('.tour-highlight')?.forEach(el => el.classList.remove('tour-highlight'));
|
|
}
|
|
|
|
complete() {
|
|
localStorage.setItem('tour_completed', 'true');
|
|
this.stop();
|
|
|
|
window.dispatchEvent(new CustomEvent('toast:show', {
|
|
detail: {
|
|
message: 'Tour abgeschlossen! Viel Erfolg mit TaskMate.',
|
|
type: 'success'
|
|
}
|
|
}));
|
|
}
|
|
|
|
next() {
|
|
if (this.currentStep < this.steps.length - 1) {
|
|
this.currentStep++;
|
|
this.showStep();
|
|
} else {
|
|
this.complete();
|
|
}
|
|
}
|
|
|
|
previous() {
|
|
if (this.currentStep > 0) {
|
|
this.currentStep--;
|
|
this.showStep();
|
|
}
|
|
}
|
|
|
|
skip() {
|
|
localStorage.setItem('tour_completed', 'true');
|
|
this.stop();
|
|
}
|
|
|
|
// =====================
|
|
// UI CREATION
|
|
// =====================
|
|
|
|
createOverlay() {
|
|
this.overlay = createElement('div', {
|
|
className: 'onboarding-overlay'
|
|
});
|
|
|
|
document.body.appendChild(this.overlay);
|
|
}
|
|
|
|
showStep() {
|
|
const step = this.steps[this.currentStep];
|
|
const targetElement = $(step.target);
|
|
|
|
if (!targetElement) {
|
|
// Skip to next step if target not found
|
|
this.next();
|
|
return;
|
|
}
|
|
|
|
// Remove previous highlight
|
|
$$('.tour-highlight')?.forEach(el => el.classList.remove('tour-highlight'));
|
|
|
|
// Highlight current target
|
|
targetElement.classList.add('tour-highlight');
|
|
|
|
// Position and show tooltip
|
|
this.showTooltip(step, targetElement);
|
|
}
|
|
|
|
showTooltip(step, targetElement) {
|
|
// Remove existing tooltip
|
|
if (this.tooltip) {
|
|
this.tooltip.remove();
|
|
}
|
|
|
|
// Create tooltip
|
|
this.tooltip = createElement('div', {
|
|
className: 'onboarding-tooltip'
|
|
});
|
|
|
|
// Content
|
|
const content = createElement('div', { className: 'onboarding-content' }, [
|
|
createElement('h3', {}, [step.title]),
|
|
createElement('p', {}, [step.content])
|
|
]);
|
|
this.tooltip.appendChild(content);
|
|
|
|
// Footer
|
|
const footer = createElement('div', { className: 'onboarding-footer' });
|
|
|
|
// Step indicator
|
|
footer.appendChild(createElement('span', {
|
|
id: 'onboarding-step'
|
|
}, [`${this.currentStep + 1} / ${this.steps.length}`]));
|
|
|
|
// Buttons
|
|
const buttons = createElement('div', { className: 'onboarding-buttons' });
|
|
|
|
if (this.currentStep > 0) {
|
|
buttons.appendChild(createElement('button', {
|
|
className: 'btn btn-ghost',
|
|
onclick: () => this.previous()
|
|
}, ['Zurück']));
|
|
}
|
|
|
|
buttons.appendChild(createElement('button', {
|
|
className: 'btn btn-ghost',
|
|
onclick: () => this.skip()
|
|
}, ['Überspringen']));
|
|
|
|
const isLast = this.currentStep === this.steps.length - 1;
|
|
buttons.appendChild(createElement('button', {
|
|
className: 'btn btn-primary',
|
|
onclick: () => this.next()
|
|
}, [isLast ? 'Fertig' : 'Weiter']));
|
|
|
|
footer.appendChild(buttons);
|
|
this.tooltip.appendChild(footer);
|
|
|
|
document.body.appendChild(this.tooltip);
|
|
|
|
// Position tooltip
|
|
this.positionTooltip(targetElement, step.position);
|
|
}
|
|
|
|
positionTooltip(targetElement, position) {
|
|
const targetRect = targetElement.getBoundingClientRect();
|
|
const tooltipRect = this.tooltip.getBoundingClientRect();
|
|
|
|
const padding = 16;
|
|
let top, left;
|
|
|
|
switch (position) {
|
|
case 'top':
|
|
top = targetRect.top - tooltipRect.height - padding;
|
|
left = targetRect.left + (targetRect.width - tooltipRect.width) / 2;
|
|
break;
|
|
|
|
case 'bottom':
|
|
top = targetRect.bottom + padding;
|
|
left = targetRect.left + (targetRect.width - tooltipRect.width) / 2;
|
|
break;
|
|
|
|
case 'left':
|
|
top = targetRect.top + (targetRect.height - tooltipRect.height) / 2;
|
|
left = targetRect.left - tooltipRect.width - padding;
|
|
break;
|
|
|
|
case 'right':
|
|
top = targetRect.top + (targetRect.height - tooltipRect.height) / 2;
|
|
left = targetRect.right + padding;
|
|
break;
|
|
|
|
case 'bottom-left':
|
|
top = targetRect.bottom + padding;
|
|
left = targetRect.right - tooltipRect.width;
|
|
break;
|
|
|
|
case 'bottom-right':
|
|
top = targetRect.bottom + padding;
|
|
left = targetRect.left;
|
|
break;
|
|
|
|
default:
|
|
top = targetRect.bottom + padding;
|
|
left = targetRect.left;
|
|
}
|
|
|
|
// Keep within viewport
|
|
const viewportWidth = window.innerWidth;
|
|
const viewportHeight = window.innerHeight;
|
|
|
|
if (left < padding) left = padding;
|
|
if (left + tooltipRect.width > viewportWidth - padding) {
|
|
left = viewportWidth - tooltipRect.width - padding;
|
|
}
|
|
|
|
if (top < padding) top = padding;
|
|
if (top + tooltipRect.height > viewportHeight - padding) {
|
|
top = viewportHeight - tooltipRect.height - padding;
|
|
}
|
|
|
|
this.tooltip.style.top = `${top}px`;
|
|
this.tooltip.style.left = `${left}px`;
|
|
}
|
|
|
|
// =====================
|
|
// HELPERS
|
|
// =====================
|
|
|
|
shouldShowTour() {
|
|
return localStorage.getItem('tour_completed') !== 'true';
|
|
}
|
|
|
|
resetTour() {
|
|
localStorage.removeItem('tour_completed');
|
|
}
|
|
}
|
|
|
|
// Helper function for querying multiple elements
|
|
function $$(selector) {
|
|
return Array.from(document.querySelectorAll(selector));
|
|
}
|
|
|
|
// Create and export singleton
|
|
const tourManager = new TourManager();
|
|
|
|
export default tourManager;
|